@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.
Files changed (118) hide show
  1. package/README.md +147 -57
  2. package/dist/__tests__/agent-log-issue.test.js +56 -0
  3. package/dist/__tests__/cli-entry-point.test.js +66 -17
  4. package/dist/__tests__/cli-subprocess.test.js +25 -0
  5. package/dist/__tests__/init.test.js +298 -0
  6. package/dist/__tests__/initiative-plan.test.js +340 -0
  7. package/dist/__tests__/mem-cleanup-execution.test.js +19 -0
  8. package/dist/__tests__/merge-block.test.js +220 -0
  9. package/dist/__tests__/safe-git.test.js +191 -0
  10. package/dist/__tests__/state-doctor.test.js +274 -0
  11. package/dist/__tests__/wu-done.test.js +36 -0
  12. package/dist/__tests__/wu-edit.test.js +119 -0
  13. package/dist/__tests__/wu-prep.test.js +108 -0
  14. package/dist/agent-issues-query.js +4 -3
  15. package/dist/agent-log-issue.js +25 -4
  16. package/dist/backlog-prune.js +5 -4
  17. package/dist/cli-entry-point.js +11 -1
  18. package/dist/doctor.js +368 -0
  19. package/dist/flow-bottlenecks.js +6 -5
  20. package/dist/flow-report.js +4 -3
  21. package/dist/gates.js +356 -101
  22. package/dist/guard-locked.js +4 -3
  23. package/dist/guard-worktree-commit.js +4 -3
  24. package/dist/init.js +508 -86
  25. package/dist/initiative-add-wu.js +4 -3
  26. package/dist/initiative-bulk-assign-wus.js +8 -5
  27. package/dist/initiative-create.js +73 -37
  28. package/dist/initiative-edit.js +37 -21
  29. package/dist/initiative-list.js +4 -3
  30. package/dist/initiative-plan.js +337 -0
  31. package/dist/initiative-status.js +4 -3
  32. package/dist/lane-health.js +377 -0
  33. package/dist/lane-suggest.js +382 -0
  34. package/dist/mem-checkpoint.js +2 -2
  35. package/dist/mem-cleanup.js +2 -2
  36. package/dist/mem-context.js +306 -0
  37. package/dist/mem-create.js +2 -2
  38. package/dist/mem-delete.js +293 -0
  39. package/dist/mem-inbox.js +2 -2
  40. package/dist/mem-index.js +211 -0
  41. package/dist/mem-init.js +1 -1
  42. package/dist/mem-profile.js +207 -0
  43. package/dist/mem-promote.js +254 -0
  44. package/dist/mem-ready.js +2 -2
  45. package/dist/mem-signal.js +2 -2
  46. package/dist/mem-start.js +2 -2
  47. package/dist/mem-summarize.js +2 -2
  48. package/dist/mem-triage.js +2 -2
  49. package/dist/merge-block.js +222 -0
  50. package/dist/metrics-cli.js +7 -4
  51. package/dist/metrics-snapshot.js +4 -3
  52. package/dist/orchestrate-initiative.js +10 -4
  53. package/dist/orchestrate-monitor.js +379 -31
  54. package/dist/signal-cleanup.js +296 -0
  55. package/dist/spawn-list.js +6 -5
  56. package/dist/state-bootstrap.js +5 -4
  57. package/dist/state-cleanup.js +360 -0
  58. package/dist/state-doctor-fix.js +196 -0
  59. package/dist/state-doctor.js +501 -0
  60. package/dist/validate-agent-skills.js +4 -3
  61. package/dist/validate-agent-sync.js +4 -3
  62. package/dist/validate-backlog-sync.js +4 -3
  63. package/dist/validate-skills-spec.js +4 -3
  64. package/dist/validate.js +4 -3
  65. package/dist/wu-block.js +3 -3
  66. package/dist/wu-claim.js +208 -98
  67. package/dist/wu-cleanup.js +5 -4
  68. package/dist/wu-create.js +71 -46
  69. package/dist/wu-delete.js +88 -60
  70. package/dist/wu-deps.js +6 -5
  71. package/dist/wu-done-check.js +34 -0
  72. package/dist/wu-done.js +39 -12
  73. package/dist/wu-edit.js +63 -28
  74. package/dist/wu-infer-lane.js +7 -6
  75. package/dist/wu-preflight.js +23 -81
  76. package/dist/wu-prep.js +125 -0
  77. package/dist/wu-prune.js +4 -3
  78. package/dist/wu-recover.js +88 -22
  79. package/dist/wu-repair.js +7 -6
  80. package/dist/wu-spawn.js +226 -270
  81. package/dist/wu-status.js +4 -3
  82. package/dist/wu-unblock.js +5 -5
  83. package/dist/wu-unlock-lane.js +4 -3
  84. package/dist/wu-validate.js +5 -4
  85. package/package.json +16 -7
  86. package/templates/core/.lumenflow/constraints.md.template +192 -0
  87. package/templates/core/.lumenflow/rules/git-safety.md.template +27 -0
  88. package/templates/core/.lumenflow/rules/wu-workflow.md.template +48 -0
  89. package/templates/core/AGENTS.md.template +60 -0
  90. package/templates/core/LUMENFLOW.md.template +255 -0
  91. package/templates/core/UPGRADING.md.template +121 -0
  92. package/templates/core/ai/onboarding/agent-safety-card.md.template +106 -0
  93. package/templates/core/ai/onboarding/first-wu-mistakes.md.template +198 -0
  94. package/templates/core/ai/onboarding/quick-ref-commands.md.template +186 -0
  95. package/templates/core/ai/onboarding/release-process.md.template +362 -0
  96. package/templates/core/ai/onboarding/troubleshooting-wu-done.md.template +159 -0
  97. package/templates/core/ai/onboarding/wu-create-checklist.md.template +117 -0
  98. package/templates/vendors/aider/.aider.conf.yml.template +27 -0
  99. package/templates/vendors/claude/.claude/CLAUDE.md.template +52 -0
  100. package/templates/vendors/claude/.claude/settings.json.template +49 -0
  101. package/templates/vendors/claude/.claude/skills/bug-classification/SKILL.md.template +192 -0
  102. package/templates/vendors/claude/.claude/skills/code-quality/SKILL.md.template +152 -0
  103. package/templates/vendors/claude/.claude/skills/context-management/SKILL.md.template +155 -0
  104. package/templates/vendors/claude/.claude/skills/execution-memory/SKILL.md.template +304 -0
  105. package/templates/vendors/claude/.claude/skills/frontend-design/SKILL.md.template +131 -0
  106. package/templates/vendors/claude/.claude/skills/initiative-management/SKILL.md.template +164 -0
  107. package/templates/vendors/claude/.claude/skills/library-first/SKILL.md.template +98 -0
  108. package/templates/vendors/claude/.claude/skills/lumenflow-gates/SKILL.md.template +87 -0
  109. package/templates/vendors/claude/.claude/skills/multi-agent-coordination/SKILL.md.template +84 -0
  110. package/templates/vendors/claude/.claude/skills/ops-maintenance/SKILL.md.template +254 -0
  111. package/templates/vendors/claude/.claude/skills/orchestration/SKILL.md.template +189 -0
  112. package/templates/vendors/claude/.claude/skills/tdd-workflow/SKILL.md.template +139 -0
  113. package/templates/vendors/claude/.claude/skills/worktree-discipline/SKILL.md.template +138 -0
  114. package/templates/vendors/claude/.claude/skills/wu-lifecycle/SKILL.md.template +106 -0
  115. package/templates/vendors/cline/.clinerules.template +53 -0
  116. package/templates/vendors/cursor/.cursor/rules/lumenflow.md.template +34 -0
  117. package/templates/vendors/cursor/.cursor/rules.md.template +28 -0
  118. 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 using mem:inbox signals.
6
- * Designed to prevent context exhaustion by using compact signal output.
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 --since 30m
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/dist/wu-constants.js';
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
- if (!existsSync(LUMENFLOW_PATHS.MEMORY_DIR)) {
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(LUMENFLOW_PATHS.MEMORY_DIR).filter((f) => f.endsWith('.ndjson'));
125
+ const files = readdirSync(memoryDir).filter((f) => f.endsWith('.ndjson'));
35
126
  for (const file of files) {
36
- const filePath = join(LUMENFLOW_PATHS.MEMORY_DIR, file);
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
- .action((opts) => {
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
- const since = parseTimeString(opts.since);
62
- console.log(chalk.cyan(`${LOG_PREFIX} Loading signals since ${since.toISOString()}...`));
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
- const filtered = opts.wu ? signals.filter((s) => s.wuId === opts.wu) : signals;
70
- console.log(chalk.bold(`\nRecent Signals (${filtered.length}):\n`));
71
- for (const signal of filtered) {
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
- console.log('');
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
- console.error(chalk.red(`${LOG_PREFIX} Error: ${err.message}`));
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
  });