agileflow 2.99.7 → 3.0.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 (65) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/lib/cache-provider.js +155 -0
  3. package/lib/codebase-indexer.js +1 -1
  4. package/lib/content-sanitizer.js +1 -0
  5. package/lib/dashboard-protocol.js +25 -0
  6. package/lib/dashboard-server.js +184 -133
  7. package/lib/errors.js +18 -0
  8. package/lib/file-cache.js +1 -1
  9. package/lib/flag-detection.js +11 -20
  10. package/lib/git-operations.js +15 -33
  11. package/lib/merge-operations.js +40 -34
  12. package/lib/process-executor.js +199 -0
  13. package/lib/registry-cache.js +13 -47
  14. package/lib/skill-loader.js +206 -0
  15. package/lib/smart-json-file.js +2 -4
  16. package/package.json +1 -1
  17. package/scripts/agileflow-configure.js +13 -12
  18. package/scripts/agileflow-statusline.sh +30 -0
  19. package/scripts/agileflow-welcome.js +181 -212
  20. package/scripts/auto-self-improve.js +3 -3
  21. package/scripts/claude-smart.sh +67 -0
  22. package/scripts/claude-tmux.sh +248 -170
  23. package/scripts/damage-control-multi-agent.js +227 -0
  24. package/scripts/lib/bus-utils.js +471 -0
  25. package/scripts/lib/configure-detect.js +5 -6
  26. package/scripts/lib/configure-features.js +44 -0
  27. package/scripts/lib/configure-repair.js +5 -6
  28. package/scripts/lib/configure-utils.js +2 -3
  29. package/scripts/lib/context-formatter.js +87 -8
  30. package/scripts/lib/damage-control-utils.js +37 -3
  31. package/scripts/lib/file-lock.js +392 -0
  32. package/scripts/lib/ideation-index.js +2 -5
  33. package/scripts/lib/lifecycle-detector.js +123 -0
  34. package/scripts/lib/process-cleanup.js +55 -81
  35. package/scripts/lib/scale-detector.js +357 -0
  36. package/scripts/lib/signal-detectors.js +779 -0
  37. package/scripts/lib/story-state-machine.js +1 -1
  38. package/scripts/lib/sync-ideation-status.js +2 -3
  39. package/scripts/lib/task-registry.js +7 -1
  40. package/scripts/lib/team-events.js +357 -0
  41. package/scripts/messaging-bridge.js +79 -36
  42. package/scripts/migrate-ideation-index.js +37 -14
  43. package/scripts/obtain-context.js +37 -19
  44. package/scripts/ralph-loop.js +3 -4
  45. package/scripts/smart-detect.js +390 -0
  46. package/scripts/team-manager.js +174 -30
  47. package/src/core/commands/audit.md +13 -11
  48. package/src/core/commands/babysit.md +162 -115
  49. package/src/core/commands/changelog.md +21 -4
  50. package/src/core/commands/configure.md +105 -2
  51. package/src/core/commands/debt.md +12 -2
  52. package/src/core/commands/feedback.md +7 -6
  53. package/src/core/commands/ideate/history.md +1 -1
  54. package/src/core/commands/ideate/new.md +5 -5
  55. package/src/core/commands/logic/audit.md +2 -2
  56. package/src/core/commands/pr.md +7 -6
  57. package/src/core/commands/research/analyze.md +28 -20
  58. package/src/core/commands/research/ask.md +43 -0
  59. package/src/core/commands/research/import.md +29 -21
  60. package/src/core/commands/research/list.md +8 -7
  61. package/src/core/commands/research/synthesize.md +356 -20
  62. package/src/core/commands/research/view.md +8 -5
  63. package/src/core/commands/review.md +24 -6
  64. package/src/core/commands/skill/create.md +34 -0
  65. package/tools/cli/lib/docs-setup.js +4 -0
@@ -0,0 +1,227 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * damage-control-multi-agent.js - PreToolUse hook for Agent Teams tools
4
+ *
5
+ * Validates TeamCreate/TeamDelete, TaskCreate/TaskUpdate, and SendMessage
6
+ * operations against safety rules when native Agent Teams is enabled.
7
+ *
8
+ * Protection layers:
9
+ * 1. Tool validation: Ensure Team/Task operations have valid parameters
10
+ * 2. Message schema: SendMessage content is validated against allowlist
11
+ * 3. Rate limiting: Prevents runaway agent spawning
12
+ * 4. Permission checks: Agents cannot escalate beyond their own tool access
13
+ *
14
+ * Exit codes:
15
+ * 0 - Allow operation (or ask via JSON output)
16
+ * 2 - Block operation
17
+ *
18
+ * Usage: Configured as PreToolUse hook in .claude/settings.json
19
+ */
20
+
21
+ const fs = require('fs');
22
+ const path = require('path');
23
+
24
+ function loadDamageControlUtils() {
25
+ const candidates = [
26
+ path.join(__dirname, 'lib', 'damage-control-utils.js'),
27
+ path.join(process.cwd(), '.agileflow', 'scripts', 'lib', 'damage-control-utils.js'),
28
+ ];
29
+
30
+ for (const candidate of candidates) {
31
+ try {
32
+ if (fs.existsSync(candidate)) {
33
+ return require(candidate);
34
+ }
35
+ } catch (e) {
36
+ // Try next candidate
37
+ }
38
+ }
39
+
40
+ return null;
41
+ }
42
+
43
+ const utils = loadDamageControlUtils();
44
+ if (!utils || typeof utils.runDamageControlHook !== 'function') {
45
+ // Fail-open: never block tools because hook bootstrap failed
46
+ process.exit(0);
47
+ }
48
+
49
+ // Tools this hook handles
50
+ const MULTI_AGENT_TOOLS = [
51
+ 'TeamCreate', 'TeamDelete',
52
+ 'TaskCreate', 'TaskUpdate', 'TaskGet', 'TaskList',
53
+ 'SendMessage',
54
+ ];
55
+
56
+ // Maximum number of teams that can be active simultaneously
57
+ const MAX_CONCURRENT_TEAMS = 4;
58
+
59
+ // Maximum teammates per team
60
+ const MAX_TEAMMATES_PER_TEAM = 8;
61
+
62
+ // SendMessage content size limit (10KB)
63
+ const MAX_MESSAGE_SIZE = 10240;
64
+
65
+ // Blocked patterns in SendMessage content
66
+ const BLOCKED_MESSAGE_PATTERNS = [
67
+ // Command injection attempts
68
+ /\$\{.*\}/, // Template injection ${...}
69
+ /`[^`]*`/, // Backtick execution
70
+ /\bexec\s*\(/, // exec() calls
71
+ /\beval\s*\(/, // eval() calls
72
+ // Dangerous git operations
73
+ /\bgit\s+push\s+--force\b/i,
74
+ /\bgit\s+reset\s+--hard\b/i,
75
+ // System destructive commands
76
+ /\brm\s+-rf\s+\//,
77
+ /\bdrop\s+database\b/i,
78
+ /\bdrop\s+table\b/i,
79
+ ];
80
+
81
+ /**
82
+ * Validate a TeamCreate operation
83
+ */
84
+ function validateTeamCreate(input) {
85
+ const toolInput = input.tool_input || input;
86
+
87
+ // Check teammate count
88
+ const teammates = toolInput.teammates || [];
89
+ if (teammates.length > MAX_TEAMMATES_PER_TEAM) {
90
+ return {
91
+ action: 'block',
92
+ reason: `Team size ${teammates.length} exceeds maximum (${MAX_TEAMMATES_PER_TEAM})`,
93
+ };
94
+ }
95
+
96
+ // Check for empty team
97
+ if (teammates.length === 0) {
98
+ return {
99
+ action: 'ask',
100
+ reason: 'Creating a team with no teammates. Continue?',
101
+ };
102
+ }
103
+
104
+ return { action: 'allow' };
105
+ }
106
+
107
+ /**
108
+ * Validate a SendMessage operation
109
+ */
110
+ function validateSendMessage(input) {
111
+ const toolInput = input.tool_input || input;
112
+ const content = toolInput.message || toolInput.content || '';
113
+
114
+ // Check message size
115
+ if (content.length > MAX_MESSAGE_SIZE) {
116
+ return {
117
+ action: 'block',
118
+ reason: `Message size (${content.length} bytes) exceeds limit (${MAX_MESSAGE_SIZE})`,
119
+ };
120
+ }
121
+
122
+ // Check for blocked patterns in content
123
+ for (const pattern of BLOCKED_MESSAGE_PATTERNS) {
124
+ if (pattern.test(content)) {
125
+ return {
126
+ action: 'block',
127
+ reason: 'Message contains potentially dangerous content pattern',
128
+ detail: `Matched: ${pattern.source}`,
129
+ };
130
+ }
131
+ }
132
+
133
+ return { action: 'allow' };
134
+ }
135
+
136
+ /**
137
+ * Validate TaskCreate/TaskUpdate operations
138
+ */
139
+ function validateTaskOperation(input) {
140
+ const toolInput = input.tool_input || input;
141
+ const description = toolInput.description || toolInput.prompt || '';
142
+
143
+ // Check for secrets in task descriptions
144
+ const secretPatterns = [
145
+ /\b(?:API_KEY|SECRET|PASSWORD|TOKEN|CREDENTIALS)\s*[:=]\s*\S+/i,
146
+ /\bsk-[a-zA-Z0-9]{20,}/, // API keys starting with sk-
147
+ /\bghp_[a-zA-Z0-9]{36}/, // GitHub personal access tokens
148
+ /\bnpm_[a-zA-Z0-9]{36}/, // npm tokens
149
+ ];
150
+
151
+ for (const pattern of secretPatterns) {
152
+ if (pattern.test(description)) {
153
+ return {
154
+ action: 'block',
155
+ reason: 'Task description appears to contain secrets or credentials',
156
+ detail: 'Never pass secrets in task parameters. Use environment variables instead.',
157
+ };
158
+ }
159
+ }
160
+
161
+ return { action: 'allow' };
162
+ }
163
+
164
+ try {
165
+ utils.runDamageControlHook({
166
+ getInputValue: (input) => {
167
+ // Check if this is a multi-agent tool
168
+ const toolName = input.tool_name || '';
169
+ if (!MULTI_AGENT_TOOLS.includes(toolName)) {
170
+ return null; // Not our tool - allow
171
+ }
172
+ return input;
173
+ },
174
+
175
+ loadConfig: () => {
176
+ // Multi-agent hook uses inline rules, not YAML patterns
177
+ return {
178
+ maxTeams: MAX_CONCURRENT_TEAMS,
179
+ maxTeammates: MAX_TEAMMATES_PER_TEAM,
180
+ maxMessageSize: MAX_MESSAGE_SIZE,
181
+ };
182
+ },
183
+
184
+ validate: (input, config) => {
185
+ const toolName = input.tool_name || '';
186
+
187
+ switch (toolName) {
188
+ case 'TeamCreate':
189
+ return validateTeamCreate(input);
190
+
191
+ case 'TeamDelete':
192
+ // Always ask before deleting a team
193
+ return {
194
+ action: 'ask',
195
+ reason: 'Deleting a team will stop all teammates. Continue?',
196
+ };
197
+
198
+ case 'SendMessage':
199
+ return validateSendMessage(input);
200
+
201
+ case 'TaskCreate':
202
+ case 'TaskUpdate':
203
+ return validateTaskOperation(input);
204
+
205
+ case 'TaskGet':
206
+ case 'TaskList':
207
+ // Read operations are always allowed
208
+ return { action: 'allow' };
209
+
210
+ default:
211
+ return { action: 'allow' };
212
+ }
213
+ },
214
+
215
+ onBlock: (result, input) => {
216
+ const toolName = input.tool_name || 'unknown';
217
+ utils.outputBlocked(
218
+ `${toolName}: ${result.reason}`,
219
+ result.detail || '',
220
+ 'Multi-agent damage control'
221
+ );
222
+ },
223
+ });
224
+ } catch (e) {
225
+ // Fail-open on runtime errors
226
+ process.exit(0);
227
+ }
@@ -0,0 +1,471 @@
1
+ /**
2
+ * bus-utils.js - JSONL bus log rotation utility
3
+ *
4
+ * Provides log rotation for bus/log.jsonl files to manage file size and maintain
5
+ * fast I/O performance. Archives old messages while preserving all history.
6
+ *
7
+ * Rotation Strategy:
8
+ * - When log.jsonl exceeds threshold (default 1000 lines), rotation is triggered
9
+ * - Recent lines (default 100) are kept in current log.jsonl
10
+ * - Earlier lines are appended to bus/archive/YYYY-MM-archive.jsonl
11
+ * - Archives are organized by month and never overwritten
12
+ * - Fail-safe: if anything fails, original log.jsonl is left intact
13
+ *
14
+ * Usage:
15
+ * const { getLineCount, shouldRotate, rotateLog, ensureArchiveDir } = require('./lib/bus-utils');
16
+ *
17
+ * // Check if rotation is needed
18
+ * const count = getLineCount(logPath);
19
+ * if (shouldRotate(logPath, 1000)) {
20
+ * const result = rotateLog(logPath, { keepRecent: 100 });
21
+ * if (result.ok) {
22
+ * console.log(`Rotated: ${result.archivedCount} messages archived`);
23
+ * }
24
+ * }
25
+ */
26
+
27
+ const fs = require('fs');
28
+ const path = require('path');
29
+ const readline = require('readline');
30
+
31
+ /**
32
+ * Get the line count of a JSONL file
33
+ *
34
+ * @param {string} logPath - Path to the JSONL log file
35
+ * @returns {number} Number of lines in the file (0 if file doesn't exist)
36
+ */
37
+ function getLineCount(logPath) {
38
+ try {
39
+ if (!fs.existsSync(logPath)) {
40
+ return 0;
41
+ }
42
+
43
+ const content = fs.readFileSync(logPath, 'utf8');
44
+ if (!content) {
45
+ return 0;
46
+ }
47
+
48
+ // Count newlines, but account for file that may not end in newline
49
+ const lineCount = content.split('\n').filter(line => line.trim().length > 0).length;
50
+ return lineCount;
51
+ } catch (e) {
52
+ // On error, assume we can't count lines (fail-safe: don't rotate)
53
+ return 0;
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Check if a log file should be rotated based on line count threshold
59
+ *
60
+ * @param {string} logPath - Path to the JSONL log file
61
+ * @param {number} [threshold=1000] - Line count threshold for rotation
62
+ * @returns {boolean} True if rotation is needed, false otherwise
63
+ */
64
+ function shouldRotate(logPath, threshold = 1000) {
65
+ const lineCount = getLineCount(logPath);
66
+ return lineCount > threshold;
67
+ }
68
+
69
+ /**
70
+ * Ensure the archive directory exists
71
+ *
72
+ * @param {string} logPath - Path to the log file (archive dir is relative to log file)
73
+ * @returns {{ ok: boolean, archiveDir?: string, error?: string }}
74
+ */
75
+ function ensureArchiveDir(logPath) {
76
+ try {
77
+ const logDir = path.dirname(logPath);
78
+ const archiveDir = path.join(logDir, 'archive');
79
+
80
+ if (!fs.existsSync(archiveDir)) {
81
+ fs.mkdirSync(archiveDir, { recursive: true });
82
+ }
83
+
84
+ return { ok: true, archiveDir };
85
+ } catch (e) {
86
+ return { ok: false, error: e.message };
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Get the archive file path for a given date
92
+ *
93
+ * @param {string} logPath - Path to the log file
94
+ * @param {Date} [date] - Date for archive (defaults to now)
95
+ * @returns {string} Path to the archive file (YYYY-MM-archive.jsonl)
96
+ */
97
+ function getArchiveFilePath(logPath, date = new Date()) {
98
+ const logDir = path.dirname(logPath);
99
+ const archiveDir = path.join(logDir, 'archive');
100
+
101
+ const year = date.getFullYear();
102
+ const month = String(date.getMonth() + 1).padStart(2, '0');
103
+ const archiveFilename = `${year}-${month}-archive.jsonl`;
104
+
105
+ return path.join(archiveDir, archiveFilename);
106
+ }
107
+
108
+ /**
109
+ * Read all lines from a JSONL file as objects
110
+ *
111
+ * @param {string} filePath - Path to the JSONL file
112
+ * @returns {{ ok: boolean, lines?: object[], error?: string }}
113
+ */
114
+ function readJSONLFile(filePath) {
115
+ try {
116
+ if (!fs.existsSync(filePath)) {
117
+ return { ok: true, lines: [] };
118
+ }
119
+
120
+ const content = fs.readFileSync(filePath, 'utf8');
121
+ const lines = [];
122
+
123
+ for (const line of content.split('\n')) {
124
+ if (!line.trim()) continue;
125
+
126
+ try {
127
+ const obj = JSON.parse(line);
128
+ lines.push(obj);
129
+ } catch (e) {
130
+ // Skip malformed lines (fail-safe: continue with valid lines)
131
+ continue;
132
+ }
133
+ }
134
+
135
+ return { ok: true, lines };
136
+ } catch (e) {
137
+ return { ok: false, error: e.message };
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Write lines to a JSONL file (append mode)
143
+ *
144
+ * @param {string} filePath - Path to the JSONL file
145
+ * @param {object[]} lines - Array of objects to write
146
+ * @param {object} [options] - Options
147
+ * @param {boolean} [options.append=true] - Append to file instead of overwriting
148
+ * @returns {{ ok: boolean, lineCount?: number, error?: string }}
149
+ */
150
+ function writeJSONLFile(filePath, lines, options = {}) {
151
+ try {
152
+ const { append = true } = options;
153
+
154
+ // Ensure directory exists
155
+ const dir = path.dirname(filePath);
156
+ if (!fs.existsSync(dir)) {
157
+ fs.mkdirSync(dir, { recursive: true });
158
+ }
159
+
160
+ // Convert lines to JSONL format
161
+ const jsonlContent = lines.map(obj => JSON.stringify(obj)).join('\n');
162
+
163
+ if (append && fs.existsSync(filePath)) {
164
+ // Append mode: add newline before new content if file exists
165
+ fs.appendFileSync(filePath, '\n' + jsonlContent);
166
+ } else {
167
+ // Write mode: overwrite file
168
+ fs.writeFileSync(filePath, jsonlContent);
169
+ }
170
+
171
+ return { ok: true, lineCount: lines.length };
172
+ } catch (e) {
173
+ return { ok: false, error: e.message };
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Perform log rotation: archive old messages, keep recent ones
179
+ *
180
+ * @param {string} logPath - Path to the JSONL log file
181
+ * @param {object} [options] - Options
182
+ * @param {number} [options.keepRecent=100] - Number of recent lines to keep in current log
183
+ * @param {number} [options.threshold=1000] - Minimum lines before rotating (for safety check)
184
+ * @returns {{ ok: boolean, archivedCount?: number, keptCount?: number, archiveFile?: string, error?: string }}
185
+ */
186
+ function rotateLog(logPath, options = {}) {
187
+ const { keepRecent = 100, threshold = 1000 } = options;
188
+
189
+ try {
190
+ // Safety check: only rotate if we have more than threshold
191
+ const lineCount = getLineCount(logPath);
192
+ if (lineCount <= threshold) {
193
+ return {
194
+ ok: true,
195
+ archivedCount: 0,
196
+ keptCount: lineCount,
197
+ message: 'No rotation needed',
198
+ };
199
+ }
200
+
201
+ // Step 1: Read all lines
202
+ const readResult = readJSONLFile(logPath);
203
+ if (!readResult.ok) {
204
+ return { ok: false, error: `Failed to read log: ${readResult.error}` };
205
+ }
206
+
207
+ const allLines = readResult.lines;
208
+ if (allLines.length === 0) {
209
+ return { ok: true, archivedCount: 0, keptCount: 0 };
210
+ }
211
+
212
+ // Step 2: Split into archive portion and keep-recent portion
213
+ const archiveCount = allLines.length - keepRecent;
214
+ const linesToArchive = allLines.slice(0, archiveCount);
215
+ const linesToKeep = allLines.slice(archiveCount);
216
+
217
+ // Step 3: Ensure archive directory exists
218
+ const archiveResult = ensureArchiveDir(logPath);
219
+ if (!archiveResult.ok) {
220
+ return { ok: false, error: `Failed to create archive directory: ${archiveResult.error}` };
221
+ }
222
+
223
+ // Step 4: Append to archive file
224
+ const archiveFilePath = getArchiveFilePath(logPath);
225
+ const appendResult = writeJSONLFile(archiveFilePath, linesToArchive, { append: true });
226
+ if (!appendResult.ok) {
227
+ return { ok: false, error: `Failed to write archive: ${appendResult.error}` };
228
+ }
229
+
230
+ // Step 5: Write kept lines back to current log (truncate + write)
231
+ const writeResult = writeJSONLFile(logPath, linesToKeep, { append: false });
232
+ if (!writeResult.ok) {
233
+ return { ok: false, error: `Failed to write current log: ${writeResult.error}` };
234
+ }
235
+
236
+ return {
237
+ ok: true,
238
+ archivedCount: linesToArchive.length,
239
+ keptCount: linesToKeep.length,
240
+ archiveFile: path.relative(path.dirname(logPath), archiveFilePath),
241
+ };
242
+ } catch (e) {
243
+ // Fail-safe: return error without modifying files
244
+ return { ok: false, error: e.message };
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Get statistics about a log file and its archives
250
+ *
251
+ * @param {string} logPath - Path to the JSONL log file
252
+ * @returns {{ ok: boolean, stats?: object, error?: string }}
253
+ */
254
+ function getLogStats(logPath) {
255
+ try {
256
+ const archiveResult = ensureArchiveDir(logPath);
257
+ if (!archiveResult.ok) {
258
+ return { ok: false, error: archiveResult.error };
259
+ }
260
+
261
+ const archiveDir = archiveResult.archiveDir;
262
+ const logDir = path.dirname(logPath);
263
+
264
+ // Count lines in current log
265
+ const currentLineCount = getLineCount(logPath);
266
+ const currentSize = fs.existsSync(logPath) ? fs.statSync(logPath).size : 0;
267
+
268
+ // Count archives
269
+ const archives = [];
270
+ if (fs.existsSync(archiveDir)) {
271
+ const files = fs.readdirSync(archiveDir);
272
+ for (const file of files) {
273
+ if (!file.endsWith('-archive.jsonl')) continue;
274
+
275
+ const filePath = path.join(archiveDir, file);
276
+ const stat = fs.statSync(filePath);
277
+ const lineCount = getLineCount(filePath);
278
+
279
+ archives.push({
280
+ filename: file,
281
+ size: stat.size,
282
+ lineCount,
283
+ modified: stat.mtime.toISOString(),
284
+ });
285
+ }
286
+ }
287
+
288
+ // Calculate totals
289
+ const totalSize = currentSize + archives.reduce((sum, a) => sum + a.size, 0);
290
+ const totalLineCount = currentLineCount + archives.reduce((sum, a) => sum + a.lineCount, 0);
291
+
292
+ return {
293
+ ok: true,
294
+ stats: {
295
+ current: {
296
+ filename: path.basename(logPath),
297
+ lineCount: currentLineCount,
298
+ size: currentSize,
299
+ sizeKB: Math.round(currentSize / 1024),
300
+ },
301
+ archives: archives.sort((a, b) => b.filename.localeCompare(a.filename)),
302
+ totals: {
303
+ lineCount: totalLineCount,
304
+ size: totalSize,
305
+ sizeKB: Math.round(totalSize / 1024),
306
+ archiveCount: archives.length,
307
+ },
308
+ },
309
+ };
310
+ } catch (e) {
311
+ return { ok: false, error: e.message };
312
+ }
313
+ }
314
+
315
+ /**
316
+ * Format log statistics for display
317
+ *
318
+ * @param {object} stats - Statistics object from getLogStats()
319
+ * @returns {string} Formatted string for display
320
+ */
321
+ function formatLogStats(stats) {
322
+ if (!stats || !stats.current) {
323
+ return 'No log statistics available';
324
+ }
325
+
326
+ const lines = [];
327
+ const current = stats.current;
328
+ const totals = stats.totals;
329
+
330
+ lines.push('Bus Log Statistics');
331
+ lines.push('─'.repeat(50));
332
+
333
+ lines.push('');
334
+ lines.push('Current Log:');
335
+ lines.push(` File: ${current.filename}`);
336
+ lines.push(` Lines: ${current.lineCount}`);
337
+ lines.push(` Size: ${current.sizeKB} KB`);
338
+
339
+ if (totals.archiveCount > 0) {
340
+ lines.push('');
341
+ lines.push('Archives:');
342
+ if (stats.archives && stats.archives.length > 0) {
343
+ for (const archive of stats.archives) {
344
+ lines.push(` ${archive.filename}: ${archive.lineCount} lines (${Math.round(archive.size / 1024)} KB)`);
345
+ }
346
+ }
347
+
348
+ lines.push('');
349
+ lines.push('Totals:');
350
+ lines.push(` All messages: ${totals.lineCount}`);
351
+ lines.push(` Total size: ${totals.sizeKB} KB`);
352
+ lines.push(` Archive files: ${totals.archiveCount}`);
353
+ }
354
+
355
+ return lines.join('\n');
356
+ }
357
+
358
+ /**
359
+ * CLI interface for bus-utils
360
+ */
361
+ function main() {
362
+ const args = process.argv.slice(2);
363
+ const command = args[0];
364
+
365
+ switch (command) {
366
+ case 'count': {
367
+ const logPath = args[1];
368
+ if (!logPath) {
369
+ console.log(JSON.stringify({ ok: false, error: 'Log path required' }));
370
+ process.exit(1);
371
+ }
372
+ const count = getLineCount(logPath);
373
+ console.log(JSON.stringify({ ok: true, lineCount: count }));
374
+ break;
375
+ }
376
+
377
+ case 'should-rotate': {
378
+ const logPath = args[1];
379
+ const threshold = parseInt(args[2] || '1000', 10);
380
+ if (!logPath) {
381
+ console.log(JSON.stringify({ ok: false, error: 'Log path required' }));
382
+ process.exit(1);
383
+ }
384
+ const shouldRotateNow = shouldRotate(logPath, threshold);
385
+ console.log(JSON.stringify({ ok: true, shouldRotate: shouldRotateNow, threshold }));
386
+ break;
387
+ }
388
+
389
+ case 'rotate': {
390
+ const logPath = args[1];
391
+ const keepRecent = parseInt(args[2] || '100', 10);
392
+ if (!logPath) {
393
+ console.log(JSON.stringify({ ok: false, error: 'Log path required' }));
394
+ process.exit(1);
395
+ }
396
+ const result = rotateLog(logPath, { keepRecent });
397
+ console.log(JSON.stringify(result));
398
+ break;
399
+ }
400
+
401
+ case 'stats': {
402
+ const logPath = args[1];
403
+ if (!logPath) {
404
+ console.log(JSON.stringify({ ok: false, error: 'Log path required' }));
405
+ process.exit(1);
406
+ }
407
+ const result = getLogStats(logPath);
408
+ if (result.ok) {
409
+ console.log(formatLogStats(result.stats));
410
+ } else {
411
+ console.log(JSON.stringify(result));
412
+ }
413
+ break;
414
+ }
415
+
416
+ case 'ensure-archive': {
417
+ const logPath = args[1];
418
+ if (!logPath) {
419
+ console.log(JSON.stringify({ ok: false, error: 'Log path required' }));
420
+ process.exit(1);
421
+ }
422
+ const result = ensureArchiveDir(logPath);
423
+ console.log(JSON.stringify(result));
424
+ break;
425
+ }
426
+
427
+ case 'help':
428
+ default:
429
+ console.log(`
430
+ Bus Log Rotation Utility
431
+
432
+ Commands:
433
+ count <log-path> Get line count of log file
434
+ should-rotate <log-path> [threshold]
435
+ Check if rotation is needed (default threshold: 1000)
436
+ rotate <log-path> [keep] Rotate log, keeping N recent lines (default: 100)
437
+ stats <log-path> Show log statistics (size, archives)
438
+ ensure-archive <log-path> Create archive directory if needed
439
+ help Show this help
440
+
441
+ Examples:
442
+ node bus-utils.js count docs/09-agents/bus/log.jsonl
443
+ node bus-utils.js should-rotate docs/09-agents/bus/log.jsonl 1000
444
+ node bus-utils.js rotate docs/09-agents/bus/log.jsonl 100
445
+ node bus-utils.js stats docs/09-agents/bus/log.jsonl
446
+ `);
447
+ }
448
+ }
449
+
450
+ // Export for use as module
451
+ module.exports = {
452
+ // Core functions
453
+ getLineCount,
454
+ shouldRotate,
455
+ rotateLog,
456
+ ensureArchiveDir,
457
+ getArchiveFilePath,
458
+
459
+ // File I/O
460
+ readJSONLFile,
461
+ writeJSONLFile,
462
+
463
+ // Statistics and formatting
464
+ getLogStats,
465
+ formatLogStats,
466
+ };
467
+
468
+ // Run CLI if executed directly
469
+ if (require.main === module) {
470
+ main();
471
+ }