@lumenflow/cli 2.2.2 → 2.3.2

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 (120) 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__/release.test.js +61 -0
  10. package/dist/__tests__/safe-git.test.js +191 -0
  11. package/dist/__tests__/state-doctor.test.js +274 -0
  12. package/dist/__tests__/wu-done.test.js +36 -0
  13. package/dist/__tests__/wu-edit.test.js +119 -0
  14. package/dist/__tests__/wu-prep.test.js +108 -0
  15. package/dist/agent-issues-query.js +4 -3
  16. package/dist/agent-log-issue.js +25 -4
  17. package/dist/backlog-prune.js +5 -4
  18. package/dist/cli-entry-point.js +11 -1
  19. package/dist/doctor.js +368 -0
  20. package/dist/flow-bottlenecks.js +6 -5
  21. package/dist/flow-report.js +4 -3
  22. package/dist/gates.js +356 -101
  23. package/dist/guard-locked.js +4 -3
  24. package/dist/guard-worktree-commit.js +4 -3
  25. package/dist/init.js +517 -86
  26. package/dist/initiative-add-wu.js +4 -3
  27. package/dist/initiative-bulk-assign-wus.js +8 -5
  28. package/dist/initiative-create.js +73 -37
  29. package/dist/initiative-edit.js +37 -21
  30. package/dist/initiative-list.js +4 -3
  31. package/dist/initiative-plan.js +337 -0
  32. package/dist/initiative-status.js +4 -3
  33. package/dist/lane-health.js +377 -0
  34. package/dist/lane-suggest.js +382 -0
  35. package/dist/mem-checkpoint.js +2 -2
  36. package/dist/mem-cleanup.js +2 -2
  37. package/dist/mem-context.js +306 -0
  38. package/dist/mem-create.js +2 -2
  39. package/dist/mem-delete.js +293 -0
  40. package/dist/mem-inbox.js +2 -2
  41. package/dist/mem-index.js +211 -0
  42. package/dist/mem-init.js +1 -1
  43. package/dist/mem-profile.js +207 -0
  44. package/dist/mem-promote.js +254 -0
  45. package/dist/mem-ready.js +2 -2
  46. package/dist/mem-signal.js +2 -2
  47. package/dist/mem-start.js +2 -2
  48. package/dist/mem-summarize.js +2 -2
  49. package/dist/mem-triage.js +2 -2
  50. package/dist/merge-block.js +222 -0
  51. package/dist/metrics-cli.js +7 -4
  52. package/dist/metrics-snapshot.js +4 -3
  53. package/dist/orchestrate-initiative.js +10 -4
  54. package/dist/orchestrate-monitor.js +379 -31
  55. package/dist/release.js +69 -29
  56. package/dist/signal-cleanup.js +296 -0
  57. package/dist/spawn-list.js +6 -5
  58. package/dist/state-bootstrap.js +5 -4
  59. package/dist/state-cleanup.js +360 -0
  60. package/dist/state-doctor-fix.js +196 -0
  61. package/dist/state-doctor.js +501 -0
  62. package/dist/validate-agent-skills.js +4 -3
  63. package/dist/validate-agent-sync.js +4 -3
  64. package/dist/validate-backlog-sync.js +4 -3
  65. package/dist/validate-skills-spec.js +4 -3
  66. package/dist/validate.js +4 -3
  67. package/dist/wu-block.js +3 -3
  68. package/dist/wu-claim.js +208 -98
  69. package/dist/wu-cleanup.js +5 -4
  70. package/dist/wu-create.js +71 -46
  71. package/dist/wu-delete.js +88 -60
  72. package/dist/wu-deps.js +6 -5
  73. package/dist/wu-done-check.js +34 -0
  74. package/dist/wu-done.js +39 -12
  75. package/dist/wu-edit.js +63 -28
  76. package/dist/wu-infer-lane.js +7 -6
  77. package/dist/wu-preflight.js +23 -81
  78. package/dist/wu-prep.js +125 -0
  79. package/dist/wu-prune.js +4 -3
  80. package/dist/wu-recover.js +88 -22
  81. package/dist/wu-repair.js +7 -6
  82. package/dist/wu-spawn.js +226 -270
  83. package/dist/wu-status.js +4 -3
  84. package/dist/wu-unblock.js +5 -5
  85. package/dist/wu-unlock-lane.js +4 -3
  86. package/dist/wu-validate.js +5 -4
  87. package/package.json +16 -7
  88. package/templates/core/.lumenflow/constraints.md.template +192 -0
  89. package/templates/core/.lumenflow/rules/git-safety.md.template +27 -0
  90. package/templates/core/.lumenflow/rules/wu-workflow.md.template +48 -0
  91. package/templates/core/AGENTS.md.template +60 -0
  92. package/templates/core/LUMENFLOW.md.template +255 -0
  93. package/templates/core/UPGRADING.md.template +121 -0
  94. package/templates/core/ai/onboarding/agent-safety-card.md.template +106 -0
  95. package/templates/core/ai/onboarding/first-wu-mistakes.md.template +198 -0
  96. package/templates/core/ai/onboarding/quick-ref-commands.md.template +186 -0
  97. package/templates/core/ai/onboarding/release-process.md.template +362 -0
  98. package/templates/core/ai/onboarding/troubleshooting-wu-done.md.template +159 -0
  99. package/templates/core/ai/onboarding/wu-create-checklist.md.template +117 -0
  100. package/templates/vendors/aider/.aider.conf.yml.template +27 -0
  101. package/templates/vendors/claude/.claude/CLAUDE.md.template +52 -0
  102. package/templates/vendors/claude/.claude/settings.json.template +49 -0
  103. package/templates/vendors/claude/.claude/skills/bug-classification/SKILL.md.template +192 -0
  104. package/templates/vendors/claude/.claude/skills/code-quality/SKILL.md.template +152 -0
  105. package/templates/vendors/claude/.claude/skills/context-management/SKILL.md.template +155 -0
  106. package/templates/vendors/claude/.claude/skills/execution-memory/SKILL.md.template +304 -0
  107. package/templates/vendors/claude/.claude/skills/frontend-design/SKILL.md.template +131 -0
  108. package/templates/vendors/claude/.claude/skills/initiative-management/SKILL.md.template +164 -0
  109. package/templates/vendors/claude/.claude/skills/library-first/SKILL.md.template +98 -0
  110. package/templates/vendors/claude/.claude/skills/lumenflow-gates/SKILL.md.template +87 -0
  111. package/templates/vendors/claude/.claude/skills/multi-agent-coordination/SKILL.md.template +84 -0
  112. package/templates/vendors/claude/.claude/skills/ops-maintenance/SKILL.md.template +254 -0
  113. package/templates/vendors/claude/.claude/skills/orchestration/SKILL.md.template +189 -0
  114. package/templates/vendors/claude/.claude/skills/tdd-workflow/SKILL.md.template +139 -0
  115. package/templates/vendors/claude/.claude/skills/worktree-discipline/SKILL.md.template +138 -0
  116. package/templates/vendors/claude/.claude/skills/wu-lifecycle/SKILL.md.template +106 -0
  117. package/templates/vendors/cline/.clinerules.template +53 -0
  118. package/templates/vendors/cursor/.cursor/rules/lumenflow.md.template +34 -0
  119. package/templates/vendors/cursor/.cursor/rules.md.template +28 -0
  120. 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
  });
package/dist/release.js CHANGED
@@ -60,6 +60,42 @@ const CHANGESET_DIR = '.changeset';
60
60
  const LUMENFLOW_FORCE_ENV = 'LUMENFLOW_FORCE';
61
61
  /** Environment variable to provide reason for force bypass */
62
62
  const LUMENFLOW_FORCE_REASON_ENV = 'LUMENFLOW_FORCE_REASON';
63
+ /**
64
+ * Environment variable for WU tool identification (WU-1296)
65
+ * Pre-push hook checks this to allow approved tool operations
66
+ */
67
+ const LUMENFLOW_WU_TOOL_ENV = 'LUMENFLOW_WU_TOOL';
68
+ /**
69
+ * Release tool identifier for pre-push hook bypass (WU-1296)
70
+ * Added to ALLOWED_WU_TOOLS in pre-push.mjs
71
+ */
72
+ export const RELEASE_WU_TOOL = 'release';
73
+ /**
74
+ * Execute a function with LUMENFLOW_WU_TOOL set to 'release' (WU-1296)
75
+ *
76
+ * This allows the release command to push to main via micro-worktree
77
+ * without requiring LUMENFLOW_FORCE bypass. The pre-push hook checks
78
+ * LUMENFLOW_WU_TOOL and allows approved tools like 'release'.
79
+ *
80
+ * @param fn - Async function to execute with release env set
81
+ * @returns Result of the function
82
+ */
83
+ export async function withReleaseEnv(fn) {
84
+ const originalValue = process.env[LUMENFLOW_WU_TOOL_ENV];
85
+ try {
86
+ process.env[LUMENFLOW_WU_TOOL_ENV] = RELEASE_WU_TOOL;
87
+ return await fn();
88
+ }
89
+ finally {
90
+ // Restore original value (or delete if it wasn't set)
91
+ if (originalValue === undefined) {
92
+ delete process.env[LUMENFLOW_WU_TOOL_ENV];
93
+ }
94
+ else {
95
+ process.env[LUMENFLOW_WU_TOOL_ENV] = originalValue;
96
+ }
97
+ }
98
+ }
63
99
  /**
64
100
  * Validate that a string is a valid semver version
65
101
  *
@@ -327,35 +363,39 @@ async function main() {
327
363
  }
328
364
  else {
329
365
  console.log(`${LOG_PREFIX} Bumping versions using micro-worktree isolation...`);
330
- await withMicroWorktree({
331
- operation: OPERATION_NAME,
332
- id: `v${version}`,
333
- logPrefix: LOG_PREFIX,
334
- execute: async ({ worktreePath }) => {
335
- // Check and exit changeset pre mode if active
336
- if (isInChangesetPreMode(worktreePath)) {
337
- console.log(`${LOG_PREFIX} Detected changeset pre-release mode, exiting...`);
338
- exitChangesetPreMode(worktreePath);
339
- console.log(`${LOG_PREFIX} ✅ Exited changeset pre mode`);
340
- }
341
- // Find package paths within the worktree
342
- const worktreePackagePaths = findPackageJsonPaths(worktreePath);
343
- // Update versions
344
- console.log(`${LOG_PREFIX} Updating ${worktreePackagePaths.length} package versions...`);
345
- await updatePackageVersions(worktreePackagePaths, version);
346
- // Get relative paths for commit
347
- const relativePaths = worktreePackagePaths.map((p) => getRelativePath(p, worktreePath));
348
- // If we exited pre mode, include the deleted pre.json in files to commit
349
- // (the deletion will be staged automatically by git add -A behavior)
350
- const changesetPrePath = join(CHANGESET_DIR, CHANGESET_PRE_JSON);
351
- const filesToCommit = [...relativePaths];
352
- // Note: Deletion of pre.json is handled by git detecting the missing file
353
- console.log(`${LOG_PREFIX} Versions updated to ${version}`);
354
- return {
355
- commitMessage: buildCommitMessage(version),
356
- files: filesToCommit,
357
- };
358
- },
366
+ // WU-1296: Use withReleaseEnv to set LUMENFLOW_WU_TOOL=release
367
+ // This allows the micro-worktree push to main without LUMENFLOW_FORCE
368
+ await withReleaseEnv(async () => {
369
+ await withMicroWorktree({
370
+ operation: OPERATION_NAME,
371
+ id: `v${version}`,
372
+ logPrefix: LOG_PREFIX,
373
+ execute: async ({ worktreePath }) => {
374
+ // Check and exit changeset pre mode if active
375
+ if (isInChangesetPreMode(worktreePath)) {
376
+ console.log(`${LOG_PREFIX} Detected changeset pre-release mode, exiting...`);
377
+ exitChangesetPreMode(worktreePath);
378
+ console.log(`${LOG_PREFIX} Exited changeset pre mode`);
379
+ }
380
+ // Find package paths within the worktree
381
+ const worktreePackagePaths = findPackageJsonPaths(worktreePath);
382
+ // Update versions
383
+ console.log(`${LOG_PREFIX} Updating ${worktreePackagePaths.length} package versions...`);
384
+ await updatePackageVersions(worktreePackagePaths, version);
385
+ // Get relative paths for commit
386
+ const relativePaths = worktreePackagePaths.map((p) => getRelativePath(p, worktreePath));
387
+ // If we exited pre mode, include the deleted pre.json in files to commit
388
+ // (the deletion will be staged automatically by git add -A behavior)
389
+ const changesetPrePath = join(CHANGESET_DIR, CHANGESET_PRE_JSON);
390
+ const filesToCommit = [...relativePaths];
391
+ // Note: Deletion of pre.json is handled by git detecting the missing file
392
+ console.log(`${LOG_PREFIX} ✅ Versions updated to ${version}`);
393
+ return {
394
+ commitMessage: buildCommitMessage(version),
395
+ files: filesToCommit,
396
+ };
397
+ },
398
+ });
359
399
  });
360
400
  console.log(`${LOG_PREFIX} ✅ Version bump committed and pushed`);
361
401
  }