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.
- package/CHANGELOG.md +10 -0
- package/lib/cache-provider.js +155 -0
- package/lib/codebase-indexer.js +1 -1
- package/lib/content-sanitizer.js +1 -0
- package/lib/dashboard-protocol.js +25 -0
- package/lib/dashboard-server.js +184 -133
- package/lib/errors.js +18 -0
- package/lib/file-cache.js +1 -1
- package/lib/flag-detection.js +11 -20
- package/lib/git-operations.js +15 -33
- package/lib/merge-operations.js +40 -34
- package/lib/process-executor.js +199 -0
- package/lib/registry-cache.js +13 -47
- package/lib/skill-loader.js +206 -0
- package/lib/smart-json-file.js +2 -4
- package/package.json +1 -1
- package/scripts/agileflow-configure.js +13 -12
- package/scripts/agileflow-statusline.sh +30 -0
- package/scripts/agileflow-welcome.js +181 -212
- package/scripts/auto-self-improve.js +3 -3
- package/scripts/claude-smart.sh +67 -0
- package/scripts/claude-tmux.sh +248 -170
- package/scripts/damage-control-multi-agent.js +227 -0
- package/scripts/lib/bus-utils.js +471 -0
- package/scripts/lib/configure-detect.js +5 -6
- package/scripts/lib/configure-features.js +44 -0
- package/scripts/lib/configure-repair.js +5 -6
- package/scripts/lib/configure-utils.js +2 -3
- package/scripts/lib/context-formatter.js +87 -8
- package/scripts/lib/damage-control-utils.js +37 -3
- package/scripts/lib/file-lock.js +392 -0
- package/scripts/lib/ideation-index.js +2 -5
- package/scripts/lib/lifecycle-detector.js +123 -0
- package/scripts/lib/process-cleanup.js +55 -81
- package/scripts/lib/scale-detector.js +357 -0
- package/scripts/lib/signal-detectors.js +779 -0
- package/scripts/lib/story-state-machine.js +1 -1
- package/scripts/lib/sync-ideation-status.js +2 -3
- package/scripts/lib/task-registry.js +7 -1
- package/scripts/lib/team-events.js +357 -0
- package/scripts/messaging-bridge.js +79 -36
- package/scripts/migrate-ideation-index.js +37 -14
- package/scripts/obtain-context.js +37 -19
- package/scripts/ralph-loop.js +3 -4
- package/scripts/smart-detect.js +390 -0
- package/scripts/team-manager.js +174 -30
- package/src/core/commands/audit.md +13 -11
- package/src/core/commands/babysit.md +162 -115
- package/src/core/commands/changelog.md +21 -4
- package/src/core/commands/configure.md +105 -2
- package/src/core/commands/debt.md +12 -2
- package/src/core/commands/feedback.md +7 -6
- package/src/core/commands/ideate/history.md +1 -1
- package/src/core/commands/ideate/new.md +5 -5
- package/src/core/commands/logic/audit.md +2 -2
- package/src/core/commands/pr.md +7 -6
- package/src/core/commands/research/analyze.md +28 -20
- package/src/core/commands/research/ask.md +43 -0
- package/src/core/commands/research/import.md +29 -21
- package/src/core/commands/research/list.md +8 -7
- package/src/core/commands/research/synthesize.md +356 -20
- package/src/core/commands/research/view.md +8 -5
- package/src/core/commands/review.md +24 -6
- package/src/core/commands/skill/create.md +34 -0
- 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
|
+
}
|