@litmers/cursorflow-orchestrator 0.1.15 → 0.1.20
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 +23 -1
- package/README.md +26 -7
- package/commands/cursorflow-run.md +2 -0
- package/commands/cursorflow-triggers.md +250 -0
- package/dist/cli/clean.js +8 -7
- package/dist/cli/clean.js.map +1 -1
- package/dist/cli/index.js +5 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/init.js +20 -14
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/logs.js +64 -47
- package/dist/cli/logs.js.map +1 -1
- package/dist/cli/monitor.js +27 -17
- package/dist/cli/monitor.js.map +1 -1
- package/dist/cli/prepare.js +73 -33
- package/dist/cli/prepare.js.map +1 -1
- package/dist/cli/resume.js +193 -40
- package/dist/cli/resume.js.map +1 -1
- package/dist/cli/run.js +3 -2
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/signal.js +7 -7
- package/dist/cli/signal.js.map +1 -1
- package/dist/core/orchestrator.d.ts +2 -1
- package/dist/core/orchestrator.js +54 -93
- package/dist/core/orchestrator.js.map +1 -1
- package/dist/core/reviewer.d.ts +6 -4
- package/dist/core/reviewer.js +7 -5
- package/dist/core/reviewer.js.map +1 -1
- package/dist/core/runner.d.ts +8 -0
- package/dist/core/runner.js +219 -32
- package/dist/core/runner.js.map +1 -1
- package/dist/utils/config.js +20 -10
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/doctor.js +35 -7
- package/dist/utils/doctor.js.map +1 -1
- package/dist/utils/enhanced-logger.d.ts +2 -2
- package/dist/utils/enhanced-logger.js +114 -43
- package/dist/utils/enhanced-logger.js.map +1 -1
- package/dist/utils/git.js +163 -10
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/log-formatter.d.ts +16 -0
- package/dist/utils/log-formatter.js +194 -0
- package/dist/utils/log-formatter.js.map +1 -0
- package/dist/utils/path.d.ts +19 -0
- package/dist/utils/path.js +77 -0
- package/dist/utils/path.js.map +1 -0
- package/dist/utils/repro-thinking-logs.d.ts +1 -0
- package/dist/utils/repro-thinking-logs.js +80 -0
- package/dist/utils/repro-thinking-logs.js.map +1 -0
- package/dist/utils/state.d.ts +4 -1
- package/dist/utils/state.js +11 -8
- package/dist/utils/state.js.map +1 -1
- package/dist/utils/template.d.ts +14 -0
- package/dist/utils/template.js +122 -0
- package/dist/utils/template.js.map +1 -0
- package/dist/utils/types.d.ts +13 -0
- package/dist/utils/webhook.js +3 -0
- package/dist/utils/webhook.js.map +1 -1
- package/package.json +4 -2
- package/scripts/ai-security-check.js +3 -0
- package/scripts/local-security-gate.sh +9 -1
- package/scripts/verify-and-fix.sh +37 -0
- package/src/cli/clean.ts +8 -7
- package/src/cli/index.ts +5 -1
- package/src/cli/init.ts +19 -15
- package/src/cli/logs.ts +67 -47
- package/src/cli/monitor.ts +28 -18
- package/src/cli/prepare.ts +75 -35
- package/src/cli/resume.ts +810 -626
- package/src/cli/run.ts +3 -2
- package/src/cli/signal.ts +7 -6
- package/src/core/orchestrator.ts +68 -93
- package/src/core/reviewer.ts +14 -9
- package/src/core/runner.ts +229 -33
- package/src/utils/config.ts +19 -11
- package/src/utils/doctor.ts +38 -7
- package/src/utils/enhanced-logger.ts +117 -49
- package/src/utils/git.ts +145 -11
- package/src/utils/log-formatter.ts +162 -0
- package/src/utils/path.ts +45 -0
- package/src/utils/repro-thinking-logs.ts +54 -0
- package/src/utils/state.ts +16 -8
- package/src/utils/template.ts +92 -0
- package/src/utils/types.ts +13 -0
- package/src/utils/webhook.ts +3 -0
- package/templates/basic.json +21 -0
- package/scripts/simple-logging-test.sh +0 -97
- package/scripts/test-real-cursor-lifecycle.sh +0 -289
- package/scripts/test-real-logging.sh +0 -289
- package/scripts/test-streaming-multi-task.sh +0 -247
package/src/cli/index.ts
CHANGED
|
@@ -34,10 +34,11 @@ function printHelp(): void {
|
|
|
34
34
|
|
|
35
35
|
\x1b[1mCOMMANDS\x1b[0m
|
|
36
36
|
\x1b[33minit\x1b[0m [options] Initialize CursorFlow in project
|
|
37
|
+
\x1b[33msetup\x1b[0m [options] Install Cursor IDE commands
|
|
37
38
|
\x1b[33mprepare\x1b[0m <feature> [opts] Prepare task directory and JSON files
|
|
38
39
|
\x1b[33mrun\x1b[0m <tasks-dir> [options] Run orchestration (DAG-based)
|
|
39
40
|
\x1b[33mmonitor\x1b[0m [run-dir] [options] \x1b[36mInteractive\x1b[0m lane dashboard
|
|
40
|
-
\x1b[33mclean\x1b[0m <type> [options] Clean branches/worktrees/logs
|
|
41
|
+
\x1b[33mclean\x1b[0m <type> [options] Clean branches/worktrees/logs/tasks
|
|
41
42
|
\x1b[33mresume\x1b[0m [lane] [options] Resume lane(s) - use --all for batch resume
|
|
42
43
|
\x1b[33mdoctor\x1b[0m [options] Check environment and preflight
|
|
43
44
|
\x1b[33msignal\x1b[0m <lane> <msg> Directly intervene in a running lane
|
|
@@ -54,6 +55,9 @@ function printHelp(): void {
|
|
|
54
55
|
$ \x1b[32mcursorflow prepare NewFeature --lanes 3\x1b[0m
|
|
55
56
|
$ \x1b[32mcursorflow run _cursorflow/tasks/MyFeature/\x1b[0m
|
|
56
57
|
$ \x1b[32mcursorflow monitor latest\x1b[0m
|
|
58
|
+
$ \x1b[32mcursorflow logs --all --follow\x1b[0m
|
|
59
|
+
$ \x1b[32mcursorflow resume --all\x1b[0m
|
|
60
|
+
$ \x1b[32mcursorflow doctor\x1b[0m
|
|
57
61
|
$ \x1b[32mcursorflow models\x1b[0m
|
|
58
62
|
|
|
59
63
|
\x1b[1mDOCUMENTATION\x1b[0m
|
package/src/cli/init.ts
CHANGED
|
@@ -9,6 +9,7 @@ import * as path from 'path';
|
|
|
9
9
|
import * as logger from '../utils/logger';
|
|
10
10
|
import { findProjectRoot, createDefaultConfig, CursorFlowConfig } from '../utils/config';
|
|
11
11
|
import { setupCommands } from './setup-commands';
|
|
12
|
+
import { safeJoin } from '../utils/path';
|
|
12
13
|
|
|
13
14
|
interface InitOptions {
|
|
14
15
|
example: boolean;
|
|
@@ -84,8 +85,8 @@ Examples:
|
|
|
84
85
|
}
|
|
85
86
|
|
|
86
87
|
function createDirectories(projectRoot: string, config: CursorFlowConfig): void {
|
|
87
|
-
const tasksDir =
|
|
88
|
-
const logsDir =
|
|
88
|
+
const tasksDir = safeJoin(projectRoot, config.tasksDir);
|
|
89
|
+
const logsDir = safeJoin(projectRoot, config.logsDir);
|
|
89
90
|
|
|
90
91
|
if (!fs.existsSync(tasksDir)) {
|
|
91
92
|
fs.mkdirSync(tasksDir, { recursive: true });
|
|
@@ -103,7 +104,7 @@ function createDirectories(projectRoot: string, config: CursorFlowConfig): void
|
|
|
103
104
|
}
|
|
104
105
|
|
|
105
106
|
function createExampleTasks(projectRoot: string, config: CursorFlowConfig): void {
|
|
106
|
-
const exampleDir =
|
|
107
|
+
const exampleDir = safeJoin(projectRoot, config.tasksDir, 'example');
|
|
107
108
|
|
|
108
109
|
if (!fs.existsSync(exampleDir)) {
|
|
109
110
|
fs.mkdirSync(exampleDir, { recursive: true });
|
|
@@ -134,13 +135,13 @@ Create a simple hello.txt file with a greeting message.
|
|
|
134
135
|
]
|
|
135
136
|
};
|
|
136
137
|
|
|
137
|
-
const taskPath =
|
|
138
|
+
const taskPath = safeJoin(exampleDir, '01-hello.json');
|
|
138
139
|
fs.writeFileSync(taskPath, JSON.stringify(exampleTask, null, 2) + '\n', 'utf8');
|
|
139
140
|
|
|
140
141
|
logger.success(`Created example task: ${path.relative(projectRoot, taskPath)}`);
|
|
141
142
|
|
|
142
143
|
// Create README
|
|
143
|
-
const readmePath =
|
|
144
|
+
const readmePath = safeJoin(exampleDir, 'README.md');
|
|
144
145
|
const readme = `# Example Task
|
|
145
146
|
|
|
146
147
|
This is an example CursorFlow task to help you get started.
|
|
@@ -172,20 +173,23 @@ cursorflow run ${config.tasksDir}/example/
|
|
|
172
173
|
* Add _cursorflow to .gitignore
|
|
173
174
|
*/
|
|
174
175
|
function updateGitignore(projectRoot: string): void {
|
|
175
|
-
const gitignorePath =
|
|
176
|
+
const gitignorePath = safeJoin(projectRoot, '.gitignore');
|
|
176
177
|
const entry = '_cursorflow/';
|
|
177
178
|
|
|
178
|
-
//
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
fs.
|
|
182
|
-
|
|
183
|
-
|
|
179
|
+
// Try to read existing .gitignore (avoid TOCTOU by reading directly)
|
|
180
|
+
let content: string;
|
|
181
|
+
try {
|
|
182
|
+
content = fs.readFileSync(gitignorePath, 'utf8');
|
|
183
|
+
} catch (err: any) {
|
|
184
|
+
if (err.code === 'ENOENT') {
|
|
185
|
+
// File doesn't exist - create new .gitignore
|
|
186
|
+
fs.writeFileSync(gitignorePath, `# CursorFlow\n${entry}\n`, 'utf8');
|
|
187
|
+
logger.success('Created .gitignore with _cursorflow/');
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
throw err;
|
|
184
191
|
}
|
|
185
192
|
|
|
186
|
-
// Read existing .gitignore
|
|
187
|
-
const content = fs.readFileSync(gitignorePath, 'utf8');
|
|
188
|
-
|
|
189
193
|
// Check if already included
|
|
190
194
|
const lines = content.split('\n');
|
|
191
195
|
const hasEntry = lines.some(line => {
|
package/src/cli/logs.ts
CHANGED
|
@@ -6,12 +6,14 @@ import * as fs from 'fs';
|
|
|
6
6
|
import * as path from 'path';
|
|
7
7
|
import * as logger from '../utils/logger';
|
|
8
8
|
import { loadConfig } from '../utils/config';
|
|
9
|
+
import { safeJoin } from '../utils/path';
|
|
9
10
|
import {
|
|
10
11
|
readJsonLog,
|
|
11
12
|
exportLogs,
|
|
12
13
|
stripAnsi,
|
|
13
14
|
JsonLogEntry
|
|
14
15
|
} from '../utils/enhanced-logger';
|
|
16
|
+
import { formatPotentialJsonMessage } from '../utils/log-formatter';
|
|
15
17
|
|
|
16
18
|
interface LogsOptions {
|
|
17
19
|
runDir?: string;
|
|
@@ -29,6 +31,13 @@ interface LogsOptions {
|
|
|
29
31
|
help: boolean;
|
|
30
32
|
}
|
|
31
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Escape special regex characters to prevent regex injection
|
|
36
|
+
*/
|
|
37
|
+
function escapeRegex(str: string): string {
|
|
38
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
39
|
+
}
|
|
40
|
+
|
|
32
41
|
function printHelp(): void {
|
|
33
42
|
console.log(`
|
|
34
43
|
Usage: cursorflow logs [run-dir] [options]
|
|
@@ -107,15 +116,15 @@ function parseArgs(args: string[]): LogsOptions {
|
|
|
107
116
|
* Find the latest run directory
|
|
108
117
|
*/
|
|
109
118
|
function findLatestRunDir(logsDir: string): string | null {
|
|
110
|
-
const runsDir =
|
|
119
|
+
const runsDir = safeJoin(logsDir, 'runs');
|
|
111
120
|
if (!fs.existsSync(runsDir)) return null;
|
|
112
121
|
|
|
113
122
|
const runs = fs.readdirSync(runsDir)
|
|
114
123
|
.filter(d => d.startsWith('run-'))
|
|
115
124
|
.map(d => ({
|
|
116
125
|
name: d,
|
|
117
|
-
path:
|
|
118
|
-
mtime: fs.statSync(
|
|
126
|
+
path: safeJoin(runsDir, d),
|
|
127
|
+
mtime: fs.statSync(safeJoin(runsDir, d)).mtime.getTime()
|
|
119
128
|
}))
|
|
120
129
|
.sort((a, b) => b.mtime - a.mtime);
|
|
121
130
|
|
|
@@ -126,11 +135,11 @@ function findLatestRunDir(logsDir: string): string | null {
|
|
|
126
135
|
* List lanes in a run directory
|
|
127
136
|
*/
|
|
128
137
|
function listLanes(runDir: string): string[] {
|
|
129
|
-
const lanesDir =
|
|
138
|
+
const lanesDir = safeJoin(runDir, 'lanes');
|
|
130
139
|
if (!fs.existsSync(lanesDir)) return [];
|
|
131
140
|
|
|
132
141
|
return fs.readdirSync(lanesDir)
|
|
133
|
-
.filter(d => fs.statSync(
|
|
142
|
+
.filter(d => fs.statSync(safeJoin(lanesDir, d)).isDirectory());
|
|
134
143
|
}
|
|
135
144
|
|
|
136
145
|
/**
|
|
@@ -141,9 +150,9 @@ function displayTextLogs(
|
|
|
141
150
|
options: LogsOptions
|
|
142
151
|
): void {
|
|
143
152
|
let logFile: string;
|
|
144
|
-
const readableLog =
|
|
145
|
-
const rawLog =
|
|
146
|
-
const cleanLog =
|
|
153
|
+
const readableLog = safeJoin(laneDir, 'terminal-readable.log');
|
|
154
|
+
const rawLog = safeJoin(laneDir, 'terminal-raw.log');
|
|
155
|
+
const cleanLog = safeJoin(laneDir, 'terminal.log');
|
|
147
156
|
|
|
148
157
|
if (options.raw) {
|
|
149
158
|
logFile = rawLog;
|
|
@@ -164,10 +173,10 @@ function displayTextLogs(
|
|
|
164
173
|
let content = fs.readFileSync(logFile, 'utf8');
|
|
165
174
|
let lines = content.split('\n');
|
|
166
175
|
|
|
167
|
-
// Apply filter
|
|
176
|
+
// Apply filter (case-insensitive string match to avoid ReDoS)
|
|
168
177
|
if (options.filter) {
|
|
169
|
-
const
|
|
170
|
-
lines = lines.filter(line =>
|
|
178
|
+
const filterLower = options.filter.toLowerCase();
|
|
179
|
+
lines = lines.filter(line => line.toLowerCase().includes(filterLower));
|
|
171
180
|
}
|
|
172
181
|
|
|
173
182
|
// Apply tail
|
|
@@ -190,7 +199,7 @@ function displayJsonLogs(
|
|
|
190
199
|
laneDir: string,
|
|
191
200
|
options: LogsOptions
|
|
192
201
|
): void {
|
|
193
|
-
const logFile =
|
|
202
|
+
const logFile = safeJoin(laneDir, 'terminal.jsonl');
|
|
194
203
|
|
|
195
204
|
if (!fs.existsSync(logFile)) {
|
|
196
205
|
console.log('No JSON log file found.');
|
|
@@ -204,10 +213,13 @@ function displayJsonLogs(
|
|
|
204
213
|
entries = entries.filter(e => e.level === options.level);
|
|
205
214
|
}
|
|
206
215
|
|
|
207
|
-
// Apply
|
|
216
|
+
// Apply filter (case-insensitive string match to avoid ReDoS)
|
|
208
217
|
if (options.filter) {
|
|
209
|
-
const
|
|
210
|
-
entries = entries.filter(e =>
|
|
218
|
+
const filterLower = options.filter.toLowerCase();
|
|
219
|
+
entries = entries.filter(e =>
|
|
220
|
+
e.message.toLowerCase().includes(filterLower) ||
|
|
221
|
+
(e.task && e.task.toLowerCase().includes(filterLower))
|
|
222
|
+
);
|
|
211
223
|
}
|
|
212
224
|
|
|
213
225
|
// Apply tail
|
|
@@ -222,7 +234,8 @@ function displayJsonLogs(
|
|
|
222
234
|
for (const entry of entries) {
|
|
223
235
|
const levelColor = getLevelColor(entry.level);
|
|
224
236
|
const ts = new Date(entry.timestamp).toLocaleTimeString();
|
|
225
|
-
|
|
237
|
+
const formattedMsg = formatPotentialJsonMessage(entry.message);
|
|
238
|
+
console.log(`${levelColor}[${ts}] [${entry.level.toUpperCase().padEnd(6)}]${logger.COLORS.reset} ${formattedMsg}`);
|
|
226
239
|
}
|
|
227
240
|
}
|
|
228
241
|
}
|
|
@@ -283,8 +296,8 @@ function readAllLaneLogs(runDir: string): MergedLogEntry[] {
|
|
|
283
296
|
const allEntries: MergedLogEntry[] = [];
|
|
284
297
|
|
|
285
298
|
lanes.forEach((laneName, index) => {
|
|
286
|
-
const laneDir =
|
|
287
|
-
const jsonLogPath =
|
|
299
|
+
const laneDir = safeJoin(runDir, 'lanes', laneName);
|
|
300
|
+
const jsonLogPath = safeJoin(laneDir, 'terminal.jsonl');
|
|
288
301
|
|
|
289
302
|
if (fs.existsSync(jsonLogPath)) {
|
|
290
303
|
const entries = readJsonLog(jsonLogPath);
|
|
@@ -326,13 +339,13 @@ function displayMergedLogs(runDir: string, options: LogsOptions): void {
|
|
|
326
339
|
entries = entries.filter(e => e.level === options.level);
|
|
327
340
|
}
|
|
328
341
|
|
|
329
|
-
// Apply
|
|
342
|
+
// Apply filter (case-insensitive string match to avoid ReDoS)
|
|
330
343
|
if (options.filter) {
|
|
331
|
-
const
|
|
344
|
+
const filterLower = options.filter.toLowerCase();
|
|
332
345
|
entries = entries.filter(e =>
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
346
|
+
e.message.toLowerCase().includes(filterLower) ||
|
|
347
|
+
(e.task && e.task.toLowerCase().includes(filterLower)) ||
|
|
348
|
+
e.laneName.toLowerCase().includes(filterLower)
|
|
336
349
|
);
|
|
337
350
|
}
|
|
338
351
|
|
|
@@ -380,7 +393,8 @@ function displayMergedLogs(runDir: string, options: LogsOptions): void {
|
|
|
380
393
|
continue;
|
|
381
394
|
}
|
|
382
395
|
|
|
383
|
-
|
|
396
|
+
const formattedMsg = formatPotentialJsonMessage(entry.message);
|
|
397
|
+
console.log(`${logger.COLORS.gray}[${ts}]${logger.COLORS.reset} ${laneColor}[${lanePad}]${logger.COLORS.reset} ${levelColor}[${levelPad}]${logger.COLORS.reset} ${formattedMsg}`);
|
|
384
398
|
}
|
|
385
399
|
|
|
386
400
|
console.log(`\n${logger.COLORS.cyan}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${logger.COLORS.reset}`);
|
|
@@ -419,12 +433,11 @@ function followAllLogs(runDir: string, options: LogsOptions): void {
|
|
|
419
433
|
const newEntries: MergedLogEntry[] = [];
|
|
420
434
|
|
|
421
435
|
for (const lane of lanes) {
|
|
422
|
-
const laneDir =
|
|
423
|
-
const jsonLogPath =
|
|
424
|
-
|
|
425
|
-
if (!fs.existsSync(jsonLogPath)) continue;
|
|
436
|
+
const laneDir = safeJoin(runDir, 'lanes', lane);
|
|
437
|
+
const jsonLogPath = safeJoin(laneDir, 'terminal.jsonl');
|
|
426
438
|
|
|
427
439
|
try {
|
|
440
|
+
// Use statSync directly to avoid TOCTOU race condition
|
|
428
441
|
const stats = fs.statSync(jsonLogPath);
|
|
429
442
|
if (stats.size > lastPositions[lane]!) {
|
|
430
443
|
const fd = fs.openSync(jsonLogPath, 'r');
|
|
@@ -467,10 +480,12 @@ function followAllLogs(runDir: string, options: LogsOptions): void {
|
|
|
467
480
|
// Apply level filter
|
|
468
481
|
if (options.level && entry.level !== options.level) continue;
|
|
469
482
|
|
|
470
|
-
// Apply
|
|
483
|
+
// Apply filter (case-insensitive string match to avoid ReDoS)
|
|
471
484
|
if (options.filter) {
|
|
472
|
-
const
|
|
473
|
-
if (!
|
|
485
|
+
const filterLower = options.filter.toLowerCase();
|
|
486
|
+
if (!entry.message.toLowerCase().includes(filterLower) &&
|
|
487
|
+
!(entry.task && entry.task.toLowerCase().includes(filterLower)) &&
|
|
488
|
+
!entry.laneName.toLowerCase().includes(filterLower)) {
|
|
474
489
|
continue;
|
|
475
490
|
}
|
|
476
491
|
}
|
|
@@ -490,7 +505,8 @@ function followAllLogs(runDir: string, options: LogsOptions): void {
|
|
|
490
505
|
continue;
|
|
491
506
|
}
|
|
492
507
|
|
|
493
|
-
|
|
508
|
+
const formattedMsg = formatPotentialJsonMessage(entry.message);
|
|
509
|
+
console.log(`${logger.COLORS.gray}[${ts}]${logger.COLORS.reset} ${entry.laneColor}[${lanePad}]${logger.COLORS.reset} ${levelColor}[${levelPad}]${logger.COLORS.reset} ${formattedMsg}`);
|
|
494
510
|
}
|
|
495
511
|
}, 100);
|
|
496
512
|
|
|
@@ -628,7 +644,9 @@ function escapeHtml(text: string): string {
|
|
|
628
644
|
.replace(/</g, '<')
|
|
629
645
|
.replace(/>/g, '>')
|
|
630
646
|
.replace(/"/g, '"')
|
|
631
|
-
.replace(/'/g, ''')
|
|
647
|
+
.replace(/'/g, ''')
|
|
648
|
+
.replace(/`/g, '`')
|
|
649
|
+
.replace(/\//g, '/');
|
|
632
650
|
}
|
|
633
651
|
|
|
634
652
|
/**
|
|
@@ -636,9 +654,9 @@ function escapeHtml(text: string): string {
|
|
|
636
654
|
*/
|
|
637
655
|
function followLogs(laneDir: string, options: LogsOptions): void {
|
|
638
656
|
let logFile: string;
|
|
639
|
-
const readableLog =
|
|
640
|
-
const rawLog =
|
|
641
|
-
const cleanLog =
|
|
657
|
+
const readableLog = safeJoin(laneDir, 'terminal-readable.log');
|
|
658
|
+
const rawLog = safeJoin(laneDir, 'terminal-raw.log');
|
|
659
|
+
const cleanLog = safeJoin(laneDir, 'terminal.log');
|
|
642
660
|
|
|
643
661
|
if (options.raw) {
|
|
644
662
|
logFile = rawLog;
|
|
@@ -657,9 +675,11 @@ function followLogs(laneDir: string, options: LogsOptions): void {
|
|
|
657
675
|
|
|
658
676
|
let lastSize = 0;
|
|
659
677
|
try {
|
|
660
|
-
|
|
678
|
+
// Use statSync directly to avoid TOCTOU race condition
|
|
679
|
+
lastSize = fs.statSync(logFile).size;
|
|
661
680
|
} catch {
|
|
662
|
-
//
|
|
681
|
+
// File doesn't exist yet or other error - start from 0
|
|
682
|
+
lastSize = 0;
|
|
663
683
|
}
|
|
664
684
|
|
|
665
685
|
console.log(`${logger.COLORS.cyan}Following ${logFile}... (Ctrl+C to stop)${logger.COLORS.reset}\n`);
|
|
@@ -677,11 +697,11 @@ function followLogs(laneDir: string, options: LogsOptions): void {
|
|
|
677
697
|
|
|
678
698
|
let content = buffer.toString();
|
|
679
699
|
|
|
680
|
-
// Apply filter
|
|
700
|
+
// Apply filter (case-insensitive string match to avoid ReDoS)
|
|
681
701
|
if (options.filter) {
|
|
682
|
-
const
|
|
702
|
+
const filterLower = options.filter.toLowerCase();
|
|
683
703
|
const lines = content.split('\n');
|
|
684
|
-
content = lines.filter(line =>
|
|
704
|
+
content = lines.filter(line => line.toLowerCase().includes(filterLower)).join('\n');
|
|
685
705
|
}
|
|
686
706
|
|
|
687
707
|
// Clean ANSI if needed (unless raw mode)
|
|
@@ -724,11 +744,11 @@ function displaySummary(runDir: string): void {
|
|
|
724
744
|
}
|
|
725
745
|
|
|
726
746
|
for (const lane of lanes) {
|
|
727
|
-
const laneDir =
|
|
728
|
-
const cleanLog =
|
|
729
|
-
const rawLog =
|
|
730
|
-
const jsonLog =
|
|
731
|
-
const readableLog =
|
|
747
|
+
const laneDir = safeJoin(runDir, 'lanes', lane);
|
|
748
|
+
const cleanLog = safeJoin(laneDir, 'terminal.log');
|
|
749
|
+
const rawLog = safeJoin(laneDir, 'terminal-raw.log');
|
|
750
|
+
const jsonLog = safeJoin(laneDir, 'terminal.jsonl');
|
|
751
|
+
const readableLog = safeJoin(laneDir, 'terminal-readable.log');
|
|
732
752
|
|
|
733
753
|
console.log(` ${logger.COLORS.green}📁 ${lane}${logger.COLORS.reset}`);
|
|
734
754
|
|
|
@@ -829,7 +849,7 @@ async function logs(args: string[]): Promise<void> {
|
|
|
829
849
|
}
|
|
830
850
|
|
|
831
851
|
// Find lane directory
|
|
832
|
-
const laneDir =
|
|
852
|
+
const laneDir = safeJoin(runDir, 'lanes', options.lane);
|
|
833
853
|
if (!fs.existsSync(laneDir)) {
|
|
834
854
|
const lanes = listLanes(runDir);
|
|
835
855
|
throw new Error(`Lane not found: ${options.lane}\nAvailable lanes: ${lanes.join(', ')}`);
|
package/src/cli/monitor.ts
CHANGED
|
@@ -9,6 +9,7 @@ import * as logger from '../utils/logger';
|
|
|
9
9
|
import { loadState, readLog } from '../utils/state';
|
|
10
10
|
import { LaneState, ConversationEntry } from '../utils/types';
|
|
11
11
|
import { loadConfig } from '../utils/config';
|
|
12
|
+
import { safeJoin } from '../utils/path';
|
|
12
13
|
|
|
13
14
|
interface LaneWithDeps {
|
|
14
15
|
name: string;
|
|
@@ -339,11 +340,11 @@ class InteractiveMonitor {
|
|
|
339
340
|
if (!lane) return;
|
|
340
341
|
|
|
341
342
|
try {
|
|
342
|
-
const interventionPath =
|
|
343
|
+
const interventionPath = safeJoin(lane.path, 'intervention.txt');
|
|
343
344
|
fs.writeFileSync(interventionPath, message, 'utf8');
|
|
344
345
|
|
|
345
346
|
// Also log it to the conversation
|
|
346
|
-
const convoPath =
|
|
347
|
+
const convoPath = safeJoin(lane.path, 'conversation.jsonl');
|
|
347
348
|
const entry = {
|
|
348
349
|
timestamp: new Date().toISOString(),
|
|
349
350
|
role: 'user',
|
|
@@ -372,7 +373,7 @@ class InteractiveMonitor {
|
|
|
372
373
|
return;
|
|
373
374
|
}
|
|
374
375
|
|
|
375
|
-
const timeoutPath =
|
|
376
|
+
const timeoutPath = safeJoin(lane.path, 'timeout.txt');
|
|
376
377
|
fs.writeFileSync(timeoutPath, String(timeoutMs), 'utf8');
|
|
377
378
|
|
|
378
379
|
this.showNotification(`Timeout updated to ${Math.round(timeoutMs/1000)}s`, 'success');
|
|
@@ -385,7 +386,7 @@ class InteractiveMonitor {
|
|
|
385
386
|
if (!this.selectedLaneName) return;
|
|
386
387
|
const lane = this.lanes.find(l => l.name === this.selectedLaneName);
|
|
387
388
|
if (!lane) return;
|
|
388
|
-
const convoPath =
|
|
389
|
+
const convoPath = safeJoin(lane.path, 'conversation.jsonl');
|
|
389
390
|
this.currentLogs = readLog<ConversationEntry>(convoPath);
|
|
390
391
|
// Keep selection in bounds after refresh
|
|
391
392
|
if (this.selectedMessageIndex >= this.currentLogs.length) {
|
|
@@ -505,8 +506,12 @@ class InteractiveMonitor {
|
|
|
505
506
|
nextAction = '🏁 Done';
|
|
506
507
|
}
|
|
507
508
|
} else if (status.status === 'waiting') {
|
|
508
|
-
|
|
509
|
-
|
|
509
|
+
if (status.waitingFor && status.waitingFor.length > 0) {
|
|
510
|
+
nextAction = `Wait for task: ${status.waitingFor.join(', ')}`;
|
|
511
|
+
} else {
|
|
512
|
+
const missingDeps = status.dependsOn.filter((d: string) => laneStatuses[d] && laneStatuses[d].status !== 'completed');
|
|
513
|
+
nextAction = `Wait for lane: ${missingDeps.join(', ')}`;
|
|
514
|
+
}
|
|
510
515
|
} else if (status.status === 'running') {
|
|
511
516
|
nextAction = '🚀 Working...';
|
|
512
517
|
}
|
|
@@ -533,7 +538,7 @@ class InteractiveMonitor {
|
|
|
533
538
|
}
|
|
534
539
|
|
|
535
540
|
const status = this.getLaneStatus(lane.path, lane.name);
|
|
536
|
-
const logPath =
|
|
541
|
+
const logPath = safeJoin(lane.path, 'terminal.log');
|
|
537
542
|
let liveLog = '(No live terminal output)';
|
|
538
543
|
if (fs.existsSync(logPath)) {
|
|
539
544
|
const content = fs.readFileSync(logPath, 'utf8');
|
|
@@ -553,6 +558,10 @@ class InteractiveMonitor {
|
|
|
553
558
|
process.stdout.write(` Chat ID: ${status.chatId}\n`);
|
|
554
559
|
process.stdout.write(` Depends: ${status.dependsOn.join(', ') || 'None'}\n`);
|
|
555
560
|
|
|
561
|
+
if (status.waitingFor && status.waitingFor.length > 0) {
|
|
562
|
+
process.stdout.write(`\x1b[33m Wait For: ${status.waitingFor.join(', ')}\x1b[0m\n`);
|
|
563
|
+
}
|
|
564
|
+
|
|
556
565
|
if (status.error) {
|
|
557
566
|
process.stdout.write(`\x1b[31m Error: ${status.error}\x1b[0m\n`);
|
|
558
567
|
}
|
|
@@ -672,7 +681,7 @@ class InteractiveMonitor {
|
|
|
672
681
|
return;
|
|
673
682
|
}
|
|
674
683
|
|
|
675
|
-
const logPath =
|
|
684
|
+
const logPath = safeJoin(lane.path, 'terminal.log');
|
|
676
685
|
let logLines: string[] = [];
|
|
677
686
|
if (fs.existsSync(logPath)) {
|
|
678
687
|
const content = fs.readFileSync(logPath, 'utf8');
|
|
@@ -753,21 +762,21 @@ class InteractiveMonitor {
|
|
|
753
762
|
}
|
|
754
763
|
|
|
755
764
|
private listLanesWithDeps(runDir: string): LaneWithDeps[] {
|
|
756
|
-
const lanesDir =
|
|
765
|
+
const lanesDir = safeJoin(runDir, 'lanes');
|
|
757
766
|
if (!fs.existsSync(lanesDir)) return [];
|
|
758
767
|
|
|
759
768
|
const config = loadConfig();
|
|
760
|
-
const tasksDir =
|
|
769
|
+
const tasksDir = safeJoin(config.projectRoot, config.tasksDir);
|
|
761
770
|
|
|
762
771
|
const laneConfigs = this.listLaneFilesFromDir(tasksDir);
|
|
763
772
|
|
|
764
773
|
return fs.readdirSync(lanesDir)
|
|
765
|
-
.filter(d => fs.statSync(
|
|
774
|
+
.filter(d => fs.statSync(safeJoin(lanesDir, d)).isDirectory())
|
|
766
775
|
.map(name => {
|
|
767
776
|
const config = laneConfigs.find(c => c.name === name);
|
|
768
777
|
return {
|
|
769
778
|
name,
|
|
770
|
-
path:
|
|
779
|
+
path: safeJoin(lanesDir, name),
|
|
771
780
|
dependsOn: config?.dependsOn || [],
|
|
772
781
|
};
|
|
773
782
|
});
|
|
@@ -778,7 +787,7 @@ class InteractiveMonitor {
|
|
|
778
787
|
return fs.readdirSync(tasksDir)
|
|
779
788
|
.filter(f => f.endsWith('.json'))
|
|
780
789
|
.map(f => {
|
|
781
|
-
const filePath =
|
|
790
|
+
const filePath = safeJoin(tasksDir, f);
|
|
782
791
|
try {
|
|
783
792
|
const config = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
784
793
|
return { name: path.basename(f, '.json'), dependsOn: config.dependsOn || [] };
|
|
@@ -789,7 +798,7 @@ class InteractiveMonitor {
|
|
|
789
798
|
}
|
|
790
799
|
|
|
791
800
|
private getLaneStatus(lanePath: string, laneName: string) {
|
|
792
|
-
const statePath =
|
|
801
|
+
const statePath = safeJoin(lanePath, 'state.json');
|
|
793
802
|
const state = loadState<LaneState & { chatId?: string }>(statePath);
|
|
794
803
|
|
|
795
804
|
const laneInfo = this.lanes.find(l => l.name === laneName);
|
|
@@ -815,9 +824,10 @@ class InteractiveMonitor {
|
|
|
815
824
|
dependsOn,
|
|
816
825
|
duration,
|
|
817
826
|
error: state.error,
|
|
818
|
-
pid: state.pid
|
|
827
|
+
pid: state.pid,
|
|
828
|
+
waitingFor: state.waitingFor || [],
|
|
819
829
|
};
|
|
820
|
-
|
|
830
|
+
}
|
|
821
831
|
|
|
822
832
|
private formatDuration(ms: number): string {
|
|
823
833
|
if (ms <= 0) return '-';
|
|
@@ -848,11 +858,11 @@ class InteractiveMonitor {
|
|
|
848
858
|
* Find the latest run directory
|
|
849
859
|
*/
|
|
850
860
|
function findLatestRunDir(logsDir: string): string | null {
|
|
851
|
-
const runsDir =
|
|
861
|
+
const runsDir = safeJoin(logsDir, 'runs');
|
|
852
862
|
if (!fs.existsSync(runsDir)) return null;
|
|
853
863
|
const runs = fs.readdirSync(runsDir)
|
|
854
864
|
.filter(d => d.startsWith('run-'))
|
|
855
|
-
.map(d => ({ name: d, path:
|
|
865
|
+
.map(d => ({ name: d, path: safeJoin(runsDir, d), mtime: fs.statSync(safeJoin(runsDir, d)).mtime.getTime() }))
|
|
856
866
|
.sort((a, b) => b.mtime - a.mtime);
|
|
857
867
|
return runs.length > 0 ? runs[0]!.path : null;
|
|
858
868
|
}
|