@lumenflow/cli 2.2.2 → 2.3.1
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/README.md +147 -57
- package/dist/__tests__/agent-log-issue.test.js +56 -0
- package/dist/__tests__/cli-entry-point.test.js +66 -17
- package/dist/__tests__/cli-subprocess.test.js +25 -0
- package/dist/__tests__/init.test.js +298 -0
- package/dist/__tests__/initiative-plan.test.js +340 -0
- package/dist/__tests__/mem-cleanup-execution.test.js +19 -0
- package/dist/__tests__/merge-block.test.js +220 -0
- package/dist/__tests__/safe-git.test.js +191 -0
- package/dist/__tests__/state-doctor.test.js +274 -0
- package/dist/__tests__/wu-done.test.js +36 -0
- package/dist/__tests__/wu-edit.test.js +119 -0
- package/dist/__tests__/wu-prep.test.js +108 -0
- package/dist/agent-issues-query.js +4 -3
- package/dist/agent-log-issue.js +25 -4
- package/dist/backlog-prune.js +5 -4
- package/dist/cli-entry-point.js +11 -1
- package/dist/doctor.js +368 -0
- package/dist/flow-bottlenecks.js +6 -5
- package/dist/flow-report.js +4 -3
- package/dist/gates.js +356 -101
- package/dist/guard-locked.js +4 -3
- package/dist/guard-worktree-commit.js +4 -3
- package/dist/init.js +508 -86
- package/dist/initiative-add-wu.js +4 -3
- package/dist/initiative-bulk-assign-wus.js +8 -5
- package/dist/initiative-create.js +73 -37
- package/dist/initiative-edit.js +37 -21
- package/dist/initiative-list.js +4 -3
- package/dist/initiative-plan.js +337 -0
- package/dist/initiative-status.js +4 -3
- package/dist/lane-health.js +377 -0
- package/dist/lane-suggest.js +382 -0
- package/dist/mem-checkpoint.js +2 -2
- package/dist/mem-cleanup.js +2 -2
- package/dist/mem-context.js +306 -0
- package/dist/mem-create.js +2 -2
- package/dist/mem-delete.js +293 -0
- package/dist/mem-inbox.js +2 -2
- package/dist/mem-index.js +211 -0
- package/dist/mem-init.js +1 -1
- package/dist/mem-profile.js +207 -0
- package/dist/mem-promote.js +254 -0
- package/dist/mem-ready.js +2 -2
- package/dist/mem-signal.js +2 -2
- package/dist/mem-start.js +2 -2
- package/dist/mem-summarize.js +2 -2
- package/dist/mem-triage.js +2 -2
- package/dist/merge-block.js +222 -0
- package/dist/metrics-cli.js +7 -4
- package/dist/metrics-snapshot.js +4 -3
- package/dist/orchestrate-initiative.js +10 -4
- package/dist/orchestrate-monitor.js +379 -31
- package/dist/signal-cleanup.js +296 -0
- package/dist/spawn-list.js +6 -5
- package/dist/state-bootstrap.js +5 -4
- package/dist/state-cleanup.js +360 -0
- package/dist/state-doctor-fix.js +196 -0
- package/dist/state-doctor.js +501 -0
- package/dist/validate-agent-skills.js +4 -3
- package/dist/validate-agent-sync.js +4 -3
- package/dist/validate-backlog-sync.js +4 -3
- package/dist/validate-skills-spec.js +4 -3
- package/dist/validate.js +4 -3
- package/dist/wu-block.js +3 -3
- package/dist/wu-claim.js +208 -98
- package/dist/wu-cleanup.js +5 -4
- package/dist/wu-create.js +71 -46
- package/dist/wu-delete.js +88 -60
- package/dist/wu-deps.js +6 -5
- package/dist/wu-done-check.js +34 -0
- package/dist/wu-done.js +39 -12
- package/dist/wu-edit.js +63 -28
- package/dist/wu-infer-lane.js +7 -6
- package/dist/wu-preflight.js +23 -81
- package/dist/wu-prep.js +125 -0
- package/dist/wu-prune.js +4 -3
- package/dist/wu-recover.js +88 -22
- package/dist/wu-repair.js +7 -6
- package/dist/wu-spawn.js +226 -270
- package/dist/wu-status.js +4 -3
- package/dist/wu-unblock.js +5 -5
- package/dist/wu-unlock-lane.js +4 -3
- package/dist/wu-validate.js +5 -4
- package/package.json +16 -7
- package/templates/core/.lumenflow/constraints.md.template +192 -0
- package/templates/core/.lumenflow/rules/git-safety.md.template +27 -0
- package/templates/core/.lumenflow/rules/wu-workflow.md.template +48 -0
- package/templates/core/AGENTS.md.template +60 -0
- package/templates/core/LUMENFLOW.md.template +255 -0
- package/templates/core/UPGRADING.md.template +121 -0
- package/templates/core/ai/onboarding/agent-safety-card.md.template +106 -0
- package/templates/core/ai/onboarding/first-wu-mistakes.md.template +198 -0
- package/templates/core/ai/onboarding/quick-ref-commands.md.template +186 -0
- package/templates/core/ai/onboarding/release-process.md.template +362 -0
- package/templates/core/ai/onboarding/troubleshooting-wu-done.md.template +159 -0
- package/templates/core/ai/onboarding/wu-create-checklist.md.template +117 -0
- package/templates/vendors/aider/.aider.conf.yml.template +27 -0
- package/templates/vendors/claude/.claude/CLAUDE.md.template +52 -0
- package/templates/vendors/claude/.claude/settings.json.template +49 -0
- package/templates/vendors/claude/.claude/skills/bug-classification/SKILL.md.template +192 -0
- package/templates/vendors/claude/.claude/skills/code-quality/SKILL.md.template +152 -0
- package/templates/vendors/claude/.claude/skills/context-management/SKILL.md.template +155 -0
- package/templates/vendors/claude/.claude/skills/execution-memory/SKILL.md.template +304 -0
- package/templates/vendors/claude/.claude/skills/frontend-design/SKILL.md.template +131 -0
- package/templates/vendors/claude/.claude/skills/initiative-management/SKILL.md.template +164 -0
- package/templates/vendors/claude/.claude/skills/library-first/SKILL.md.template +98 -0
- package/templates/vendors/claude/.claude/skills/lumenflow-gates/SKILL.md.template +87 -0
- package/templates/vendors/claude/.claude/skills/multi-agent-coordination/SKILL.md.template +84 -0
- package/templates/vendors/claude/.claude/skills/ops-maintenance/SKILL.md.template +254 -0
- package/templates/vendors/claude/.claude/skills/orchestration/SKILL.md.template +189 -0
- package/templates/vendors/claude/.claude/skills/tdd-workflow/SKILL.md.template +139 -0
- package/templates/vendors/claude/.claude/skills/worktree-discipline/SKILL.md.template +138 -0
- package/templates/vendors/claude/.claude/skills/wu-lifecycle/SKILL.md.template +106 -0
- package/templates/vendors/cline/.clinerules.template +53 -0
- package/templates/vendors/cursor/.cursor/rules/lumenflow.md.template +34 -0
- package/templates/vendors/cursor/.cursor/rules.md.template +28 -0
- package/templates/vendors/windsurf/.windsurf/rules/lumenflow.md.template +34 -0
|
@@ -1,20 +1,109 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
/* eslint-disable no-console -- CLI tool requires console output */
|
|
2
3
|
/**
|
|
3
|
-
* Orchestrate Monitor CLI
|
|
4
|
+
* Orchestrate Monitor CLI (WU-1241)
|
|
4
5
|
*
|
|
5
|
-
* Monitors spawned agent progress
|
|
6
|
-
*
|
|
6
|
+
* Monitors spawned agent progress and spawn health.
|
|
7
|
+
* Wires CLI to spawn-monitor APIs in @lumenflow/core.
|
|
8
|
+
*
|
|
9
|
+
* Features:
|
|
10
|
+
* - Stuck detection: identifies pending spawns older than threshold
|
|
11
|
+
* - Zombie lock detection: identifies abandoned WU locks (dead PIDs)
|
|
12
|
+
* - Recovery actions: signal agent, restart spawn, escalate to human
|
|
13
|
+
* - Status reporting: active spawns, stuck spawns, zombie locks, suggestions
|
|
7
14
|
*
|
|
8
15
|
* Usage:
|
|
9
|
-
* pnpm orchestrate:monitor
|
|
16
|
+
* pnpm orchestrate:monitor # Show spawn status
|
|
17
|
+
* pnpm orchestrate:monitor --threshold 15 # Custom threshold (15 min)
|
|
18
|
+
* pnpm orchestrate:monitor --recover # Run recovery actions
|
|
19
|
+
* pnpm orchestrate:monitor --recover --dry-run # Show what would be done
|
|
20
|
+
* pnpm orchestrate:monitor --since 30m # Show signals since time
|
|
10
21
|
*/
|
|
11
22
|
import { Command } from 'commander';
|
|
12
23
|
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
13
24
|
import { join } from 'node:path';
|
|
14
|
-
import { EXIT_CODES, LUMENFLOW_PATHS } from '@lumenflow/core
|
|
25
|
+
import { EXIT_CODES, LUMENFLOW_PATHS, SpawnRegistryStore, analyzeSpawns, detectStuckSpawns, checkZombieLocks, generateSuggestions, formatMonitorOutput, formatRecoveryResults, runRecovery, processSpawnFailureSignals, formatSignalHandlerOutput, DEFAULT_THRESHOLD_MINUTES, calculateBackoff, } from '@lumenflow/core';
|
|
15
26
|
import chalk from 'chalk';
|
|
16
27
|
import ms from 'ms';
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// WU-1242: Watch Mode Constants
|
|
30
|
+
// ============================================================================
|
|
31
|
+
/**
|
|
32
|
+
* Default watch interval (5 minutes in milliseconds)
|
|
33
|
+
*/
|
|
34
|
+
export const DEFAULT_WATCH_INTERVAL_MS = 5 * 60 * 1000;
|
|
35
|
+
/**
|
|
36
|
+
* Minimum watch interval (1 minute in milliseconds)
|
|
37
|
+
*/
|
|
38
|
+
export const MIN_WATCH_INTERVAL_MS = 60 * 1000;
|
|
39
|
+
/**
|
|
40
|
+
* Maximum backoff interval (1 hour in milliseconds)
|
|
41
|
+
*/
|
|
42
|
+
export const MAX_BACKOFF_MS = 60 * 60 * 1000;
|
|
17
43
|
const LOG_PREFIX = '[orchestrate:monitor]';
|
|
44
|
+
/**
|
|
45
|
+
* Runs the spawn monitor.
|
|
46
|
+
*
|
|
47
|
+
* @param options - Monitor options
|
|
48
|
+
* @returns MonitorResult with analysis data
|
|
49
|
+
*/
|
|
50
|
+
export async function runMonitor(options = {}) {
|
|
51
|
+
const { baseDir = process.cwd(), thresholdMinutes = DEFAULT_THRESHOLD_MINUTES, recover = false, dryRun = false, } = options;
|
|
52
|
+
// Load spawn registry
|
|
53
|
+
// WU-1278: Use full LUMENFLOW_PATHS.STATE_DIR without stripping .lumenflow/ prefix
|
|
54
|
+
const stateDir = join(baseDir, LUMENFLOW_PATHS.STATE_DIR);
|
|
55
|
+
const store = new SpawnRegistryStore(stateDir);
|
|
56
|
+
let spawns = [];
|
|
57
|
+
try {
|
|
58
|
+
await store.load();
|
|
59
|
+
spawns = store.getAllSpawns();
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// Registry doesn't exist or is invalid - continue with empty spawns
|
|
63
|
+
}
|
|
64
|
+
// Run core analysis
|
|
65
|
+
const analysis = analyzeSpawns(spawns);
|
|
66
|
+
const stuckSpawns = detectStuckSpawns(spawns, thresholdMinutes);
|
|
67
|
+
const zombieLocks = await checkZombieLocks({ baseDir });
|
|
68
|
+
const suggestions = generateSuggestions(stuckSpawns, zombieLocks);
|
|
69
|
+
const result = {
|
|
70
|
+
analysis,
|
|
71
|
+
stuckSpawns,
|
|
72
|
+
zombieLocks,
|
|
73
|
+
suggestions,
|
|
74
|
+
dryRun,
|
|
75
|
+
};
|
|
76
|
+
// Run recovery if requested
|
|
77
|
+
if (recover) {
|
|
78
|
+
const recoveryResults = await runRecovery(stuckSpawns, { baseDir, dryRun });
|
|
79
|
+
result.recoveryResults = recoveryResults;
|
|
80
|
+
}
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Formats monitor result for display.
|
|
85
|
+
*
|
|
86
|
+
* @param result - Monitor result to format
|
|
87
|
+
* @returns Formatted output string
|
|
88
|
+
*/
|
|
89
|
+
export function formatOutput(result) {
|
|
90
|
+
const lines = [];
|
|
91
|
+
// Use core formatMonitorOutput for base formatting
|
|
92
|
+
const baseOutput = formatMonitorOutput(result);
|
|
93
|
+
lines.push(baseOutput);
|
|
94
|
+
// Add recovery results if present
|
|
95
|
+
if (result.recoveryResults && result.recoveryResults.length > 0) {
|
|
96
|
+
lines.push('');
|
|
97
|
+
lines.push(formatRecoveryResults(result.recoveryResults));
|
|
98
|
+
}
|
|
99
|
+
// Add dry-run notice if applicable
|
|
100
|
+
if (result.dryRun) {
|
|
101
|
+
lines.push('');
|
|
102
|
+
lines.push(chalk.yellow('=== DRY RUN MODE ==='));
|
|
103
|
+
lines.push(chalk.yellow('No actions were taken. Remove --dry-run to execute.'));
|
|
104
|
+
}
|
|
105
|
+
return lines.join('\n');
|
|
106
|
+
}
|
|
18
107
|
function parseTimeString(timeStr) {
|
|
19
108
|
const msValue = ms(timeStr);
|
|
20
109
|
if (typeof msValue === 'number') {
|
|
@@ -26,14 +115,16 @@ function parseTimeString(timeStr) {
|
|
|
26
115
|
}
|
|
27
116
|
return date;
|
|
28
117
|
}
|
|
29
|
-
function loadRecentSignals(since) {
|
|
118
|
+
function loadRecentSignals(since, baseDir = process.cwd()) {
|
|
30
119
|
const signals = [];
|
|
31
|
-
|
|
120
|
+
// WU-1278: Use full LUMENFLOW_PATHS.MEMORY_DIR without stripping .lumenflow/ prefix
|
|
121
|
+
const memoryDir = join(baseDir, LUMENFLOW_PATHS.MEMORY_DIR);
|
|
122
|
+
if (!existsSync(memoryDir)) {
|
|
32
123
|
return signals;
|
|
33
124
|
}
|
|
34
|
-
const files = readdirSync(
|
|
125
|
+
const files = readdirSync(memoryDir).filter((f) => f.endsWith('.ndjson'));
|
|
35
126
|
for (const file of files) {
|
|
36
|
-
const filePath = join(
|
|
127
|
+
const filePath = join(memoryDir, file);
|
|
37
128
|
const content = readFileSync(filePath, 'utf-8');
|
|
38
129
|
const lines = content.trim().split('\n').filter(Boolean);
|
|
39
130
|
for (const line of lines) {
|
|
@@ -51,38 +142,295 @@ function loadRecentSignals(since) {
|
|
|
51
142
|
}
|
|
52
143
|
return signals.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
|
53
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* Format signal type with appropriate color
|
|
147
|
+
*/
|
|
148
|
+
function formatSignalType(signalType) {
|
|
149
|
+
if (signalType === 'complete') {
|
|
150
|
+
return chalk.green(signalType);
|
|
151
|
+
}
|
|
152
|
+
if (signalType === 'error') {
|
|
153
|
+
return chalk.red(signalType);
|
|
154
|
+
}
|
|
155
|
+
return chalk.yellow(signalType);
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Display signals in --signals-only mode
|
|
159
|
+
*/
|
|
160
|
+
async function displaySignals(opts) {
|
|
161
|
+
const baseDir = process.cwd();
|
|
162
|
+
const since = parseTimeString(opts.since);
|
|
163
|
+
console.log(chalk.cyan(`${LOG_PREFIX} Loading signals since ${since.toISOString()}...`));
|
|
164
|
+
const signals = loadRecentSignals(since, baseDir);
|
|
165
|
+
if (signals.length === 0) {
|
|
166
|
+
console.log(chalk.yellow(`${LOG_PREFIX} No signals found.`));
|
|
167
|
+
console.log(chalk.gray('Agents may still be starting up, or memory layer not initialized.'));
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const filtered = opts.wu ? signals.filter((s) => s.wuId === opts.wu) : signals;
|
|
171
|
+
console.log(chalk.bold(`\nRecent Signals (${filtered.length}):\n`));
|
|
172
|
+
for (const signal of filtered) {
|
|
173
|
+
const time = new Date(signal.timestamp).toLocaleTimeString();
|
|
174
|
+
const wu = signal.wuId ? chalk.cyan(signal.wuId) : chalk.gray('system');
|
|
175
|
+
const type = formatSignalType(signal.type);
|
|
176
|
+
console.log(` ${chalk.gray(time)} [${wu}] ${type}: ${signal.message || ''}`);
|
|
177
|
+
}
|
|
178
|
+
console.log('');
|
|
179
|
+
console.log(chalk.gray(`Use: pnpm mem:inbox --since ${opts.since} for more details`));
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Run main spawn monitoring mode
|
|
183
|
+
*/
|
|
184
|
+
async function runSpawnMonitoring(opts) {
|
|
185
|
+
const baseDir = process.cwd();
|
|
186
|
+
const thresholdMinutes = parseInt(opts.threshold, 10);
|
|
187
|
+
if (isNaN(thresholdMinutes) || thresholdMinutes <= 0) {
|
|
188
|
+
console.error(chalk.red(`${LOG_PREFIX} Invalid threshold: ${opts.threshold}`));
|
|
189
|
+
process.exit(EXIT_CODES.FAILURE);
|
|
190
|
+
}
|
|
191
|
+
console.log(chalk.cyan(`${LOG_PREFIX} Analyzing spawn health...`));
|
|
192
|
+
console.log(chalk.gray(` Threshold: ${thresholdMinutes} minutes`));
|
|
193
|
+
console.log(chalk.gray(` Recovery: ${opts.recover ? 'enabled' : 'disabled'}`));
|
|
194
|
+
console.log(chalk.gray(` Dry-run: ${opts.dryRun ? 'yes' : 'no'}`));
|
|
195
|
+
console.log('');
|
|
196
|
+
const result = await runMonitor({
|
|
197
|
+
baseDir,
|
|
198
|
+
thresholdMinutes,
|
|
199
|
+
recover: opts.recover,
|
|
200
|
+
dryRun: opts.dryRun,
|
|
201
|
+
});
|
|
202
|
+
console.log(formatOutput(result));
|
|
203
|
+
if (opts.recover) {
|
|
204
|
+
console.log('');
|
|
205
|
+
console.log(chalk.cyan(`${LOG_PREFIX} Processing spawn failure signals...`));
|
|
206
|
+
const signalResult = await processSpawnFailureSignals({ baseDir, dryRun: opts.dryRun });
|
|
207
|
+
console.log(formatSignalHandlerOutput(signalResult));
|
|
208
|
+
}
|
|
209
|
+
if (result.stuckSpawns.length > 0 || result.zombieLocks.length > 0) {
|
|
210
|
+
process.exit(EXIT_CODES.ERROR);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Parses CLI arguments into watch mode options.
|
|
215
|
+
*
|
|
216
|
+
* @param opts - Raw CLI options
|
|
217
|
+
* @returns Parsed watch mode options
|
|
218
|
+
*/
|
|
219
|
+
export function parseWatchOptions(opts) {
|
|
220
|
+
let intervalMs = DEFAULT_WATCH_INTERVAL_MS;
|
|
221
|
+
if (opts.interval) {
|
|
222
|
+
// Check if it's a plain number (no unit suffix) - treat as minutes
|
|
223
|
+
if (/^\d+$/.test(opts.interval)) {
|
|
224
|
+
const minutes = parseInt(opts.interval, 10);
|
|
225
|
+
intervalMs = minutes * 60 * 1000;
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
// Parse with ms library (handles units like "5m", "1h", "30s")
|
|
229
|
+
const parsed = ms(opts.interval);
|
|
230
|
+
if (typeof parsed === 'number') {
|
|
231
|
+
intervalMs = parsed;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
// Enforce minimum interval
|
|
236
|
+
if (intervalMs < MIN_WATCH_INTERVAL_MS) {
|
|
237
|
+
intervalMs = MIN_WATCH_INTERVAL_MS;
|
|
238
|
+
}
|
|
239
|
+
return { intervalMs };
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Formats watch cycle output for display.
|
|
243
|
+
*
|
|
244
|
+
* @param result - Monitor result from the cycle
|
|
245
|
+
* @param cycleNumber - The cycle number (1-indexed)
|
|
246
|
+
* @param timestamp - Timestamp of the cycle
|
|
247
|
+
* @returns Formatted output string
|
|
248
|
+
*/
|
|
249
|
+
export function formatWatchCycleOutput(result, cycleNumber, timestamp) {
|
|
250
|
+
const lines = [];
|
|
251
|
+
// Cycle header with timestamp
|
|
252
|
+
const timeStr = timestamp.toISOString().replace('T', ' ').substring(0, 19);
|
|
253
|
+
lines.push(chalk.cyan(`=== Patrol Cycle #${cycleNumber} [${timeStr}] ===`));
|
|
254
|
+
lines.push('');
|
|
255
|
+
// Quick summary
|
|
256
|
+
const { analysis, stuckSpawns, zombieLocks } = result;
|
|
257
|
+
const statusLine = [
|
|
258
|
+
`Pending: ${analysis.pending}`,
|
|
259
|
+
`Completed: ${analysis.completed}`,
|
|
260
|
+
`Stuck: ${stuckSpawns.length}`,
|
|
261
|
+
`Zombies: ${zombieLocks.length}`,
|
|
262
|
+
].join(' | ');
|
|
263
|
+
if (stuckSpawns.length === 0 && zombieLocks.length === 0) {
|
|
264
|
+
lines.push(chalk.green(` ${statusLine}`));
|
|
265
|
+
lines.push(chalk.green(' All spawns healthy.'));
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
lines.push(chalk.yellow(` ${statusLine}`));
|
|
269
|
+
// Show stuck spawns
|
|
270
|
+
if (stuckSpawns.length > 0) {
|
|
271
|
+
lines.push('');
|
|
272
|
+
lines.push(chalk.yellow(' Stuck spawns:'));
|
|
273
|
+
for (const info of stuckSpawns) {
|
|
274
|
+
lines.push(chalk.yellow(` - ${info.spawn.targetWuId} (${info.ageMinutes}min)`));
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
// Show zombie locks
|
|
278
|
+
if (zombieLocks.length > 0) {
|
|
279
|
+
lines.push('');
|
|
280
|
+
lines.push(chalk.yellow(' Zombie locks:'));
|
|
281
|
+
for (const lock of zombieLocks) {
|
|
282
|
+
lines.push(chalk.yellow(` - ${lock.lane} (PID ${lock.pid})`));
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
lines.push('');
|
|
287
|
+
return lines.join('\n');
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Creates a watch mode runner for continuous spawn monitoring.
|
|
291
|
+
*
|
|
292
|
+
* @param options - Configuration options
|
|
293
|
+
* @returns WatchModeRunner instance
|
|
294
|
+
*/
|
|
295
|
+
export function createWatchModeRunner(options) {
|
|
296
|
+
const { checkFn, intervalMs, onOutput = console.log } = options;
|
|
297
|
+
let currentIntervalMs = intervalMs;
|
|
298
|
+
let consecutiveFailures = 0;
|
|
299
|
+
let running = false;
|
|
300
|
+
let timer = null;
|
|
301
|
+
let cycleCount = 0;
|
|
302
|
+
function scheduleNext() {
|
|
303
|
+
if (!running)
|
|
304
|
+
return;
|
|
305
|
+
timer = setTimeout(() => {
|
|
306
|
+
void runCycle().then(() => scheduleNext());
|
|
307
|
+
}, currentIntervalMs);
|
|
308
|
+
}
|
|
309
|
+
async function runCycle() {
|
|
310
|
+
if (!running)
|
|
311
|
+
return;
|
|
312
|
+
cycleCount++;
|
|
313
|
+
const cycleNumber = cycleCount;
|
|
314
|
+
try {
|
|
315
|
+
const result = await checkFn();
|
|
316
|
+
// Success - reset backoff
|
|
317
|
+
consecutiveFailures = 0;
|
|
318
|
+
currentIntervalMs = intervalMs;
|
|
319
|
+
// Output cycle result
|
|
320
|
+
const output = formatWatchCycleOutput(result, cycleNumber, new Date());
|
|
321
|
+
onOutput(output);
|
|
322
|
+
}
|
|
323
|
+
catch (error) {
|
|
324
|
+
// Failure - apply backoff
|
|
325
|
+
consecutiveFailures++;
|
|
326
|
+
currentIntervalMs = calculateBackoff(consecutiveFailures, intervalMs);
|
|
327
|
+
if (currentIntervalMs > MAX_BACKOFF_MS) {
|
|
328
|
+
currentIntervalMs = MAX_BACKOFF_MS;
|
|
329
|
+
}
|
|
330
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
331
|
+
onOutput(chalk.red(`=== Patrol Cycle #${cycleNumber} ERROR ===`));
|
|
332
|
+
onOutput(chalk.red(` ${errorMsg}`));
|
|
333
|
+
onOutput(chalk.yellow(` Next check in ${Math.round(currentIntervalMs / 1000)}s (backoff)`));
|
|
334
|
+
onOutput('');
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return {
|
|
338
|
+
start() {
|
|
339
|
+
if (running)
|
|
340
|
+
return;
|
|
341
|
+
running = true;
|
|
342
|
+
onOutput(chalk.cyan(`${LOG_PREFIX} Starting watch mode (interval: ${intervalMs / 1000}s)`));
|
|
343
|
+
onOutput('');
|
|
344
|
+
scheduleNext();
|
|
345
|
+
},
|
|
346
|
+
stop() {
|
|
347
|
+
running = false;
|
|
348
|
+
if (timer) {
|
|
349
|
+
clearTimeout(timer);
|
|
350
|
+
timer = null;
|
|
351
|
+
}
|
|
352
|
+
onOutput(chalk.cyan(`${LOG_PREFIX} Stopping watch mode. Exiting gracefully.`));
|
|
353
|
+
},
|
|
354
|
+
get isRunning() {
|
|
355
|
+
return running;
|
|
356
|
+
},
|
|
357
|
+
get currentIntervalMs() {
|
|
358
|
+
return currentIntervalMs;
|
|
359
|
+
},
|
|
360
|
+
get consecutiveFailures() {
|
|
361
|
+
return consecutiveFailures;
|
|
362
|
+
},
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Run watch mode (continuous patrol)
|
|
367
|
+
*/
|
|
368
|
+
async function runWatchMode(opts) {
|
|
369
|
+
const baseDir = process.cwd();
|
|
370
|
+
const thresholdMinutes = parseInt(opts.threshold, 10);
|
|
371
|
+
if (isNaN(thresholdMinutes) || thresholdMinutes <= 0) {
|
|
372
|
+
console.error(chalk.red(`${LOG_PREFIX} Invalid threshold: ${opts.threshold}`));
|
|
373
|
+
process.exit(EXIT_CODES.FAILURE);
|
|
374
|
+
}
|
|
375
|
+
const watchOptions = parseWatchOptions(opts);
|
|
376
|
+
console.log(chalk.cyan(`${LOG_PREFIX} Starting continuous patrol mode...`));
|
|
377
|
+
console.log(chalk.gray(` Threshold: ${thresholdMinutes} minutes`));
|
|
378
|
+
console.log(chalk.gray(` Interval: ${watchOptions.intervalMs / 1000} seconds`));
|
|
379
|
+
console.log(chalk.gray(` Recovery: ${opts.recover ? 'enabled' : 'disabled'}`));
|
|
380
|
+
console.log(chalk.gray(` Press Ctrl+C to stop`));
|
|
381
|
+
console.log('');
|
|
382
|
+
const checkFn = async () => {
|
|
383
|
+
return runMonitor({
|
|
384
|
+
baseDir,
|
|
385
|
+
thresholdMinutes,
|
|
386
|
+
recover: opts.recover,
|
|
387
|
+
dryRun: opts.dryRun,
|
|
388
|
+
});
|
|
389
|
+
};
|
|
390
|
+
const runner = createWatchModeRunner({
|
|
391
|
+
checkFn,
|
|
392
|
+
intervalMs: watchOptions.intervalMs,
|
|
393
|
+
});
|
|
394
|
+
// Handle graceful shutdown
|
|
395
|
+
const shutdown = () => {
|
|
396
|
+
runner.stop();
|
|
397
|
+
process.exit(0);
|
|
398
|
+
};
|
|
399
|
+
process.on('SIGINT', shutdown);
|
|
400
|
+
process.on('SIGTERM', shutdown);
|
|
401
|
+
runner.start();
|
|
402
|
+
// Keep process alive
|
|
403
|
+
await new Promise(() => {
|
|
404
|
+
// Never resolves - waits for signal
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
// CLI program
|
|
54
408
|
const program = new Command()
|
|
55
409
|
.name('orchestrate:monitor')
|
|
56
|
-
.description('Monitor spawned agent progress')
|
|
410
|
+
.description('Monitor spawned agent progress and spawn health (WU-1241, WU-1242)')
|
|
411
|
+
.option('--threshold <minutes>', 'Stuck detection threshold in minutes (default: 30)', '30')
|
|
412
|
+
.option('--recover', 'Run recovery actions for stuck spawns', false)
|
|
413
|
+
.option('--dry-run', 'Show what would be done without taking action', false)
|
|
57
414
|
.option('--since <time>', 'Show signals since (e.g., 30m, 1h)', '30m')
|
|
58
415
|
.option('--wu <id>', 'Filter by WU ID')
|
|
59
|
-
.
|
|
416
|
+
.option('--signals-only', 'Only show signals (skip spawn analysis)', false)
|
|
417
|
+
.option('--watch', 'Continuous patrol mode (WU-1242)', false)
|
|
418
|
+
.option('--interval <time>', 'Patrol interval for watch mode (e.g., 5m, 10m, 1h)', '5m')
|
|
419
|
+
.action(async (opts) => {
|
|
60
420
|
try {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const signals = loadRecentSignals(since);
|
|
64
|
-
if (signals.length === 0) {
|
|
65
|
-
console.log(chalk.yellow(`${LOG_PREFIX} No signals found.`));
|
|
66
|
-
console.log(chalk.gray('Agents may still be starting up, or memory layer not initialized.'));
|
|
421
|
+
if (opts.signalsOnly) {
|
|
422
|
+
await displaySignals(opts);
|
|
67
423
|
return;
|
|
68
424
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const time = new Date(signal.timestamp).toLocaleTimeString();
|
|
73
|
-
const wu = signal.wuId ? chalk.cyan(signal.wuId) : chalk.gray('system');
|
|
74
|
-
const type = signal.type === 'complete'
|
|
75
|
-
? chalk.green(signal.type)
|
|
76
|
-
: signal.type === 'error'
|
|
77
|
-
? chalk.red(signal.type)
|
|
78
|
-
: chalk.yellow(signal.type);
|
|
79
|
-
console.log(` ${chalk.gray(time)} [${wu}] ${type}: ${signal.message || ''}`);
|
|
425
|
+
if (opts.watch) {
|
|
426
|
+
await runWatchMode(opts);
|
|
427
|
+
return;
|
|
80
428
|
}
|
|
81
|
-
|
|
82
|
-
console.log(chalk.gray(`Use: pnpm mem:inbox --since ${opts.since} for more details`));
|
|
429
|
+
await runSpawnMonitoring(opts);
|
|
83
430
|
}
|
|
84
431
|
catch (err) {
|
|
85
|
-
|
|
432
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
433
|
+
console.error(chalk.red(`${LOG_PREFIX} Error: ${message}`));
|
|
86
434
|
process.exit(EXIT_CODES.ERROR);
|
|
87
435
|
}
|
|
88
436
|
});
|