@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
package/dist/mem-start.js CHANGED
@@ -10,8 +10,8 @@
10
10
  * Usage:
11
11
  * pnpm mem:start --wu WU-1234 [--agent-type <type>] [--context-tier <tier>] [--quiet]
12
12
  *
13
- * @see {@link tools/lib/mem-start-core.mjs} - Core logic
14
- * @see {@link tools/__tests__/mem-start.test.mjs} - Tests
13
+ * @see {@link packages/@lumenflow/cli/src/lib/mem-start-core.ts} - Core logic
14
+ * @see {@link packages/@lumenflow/cli/src/__tests__/mem-start.test.ts} - Tests
15
15
  */
16
16
  import fs from 'node:fs/promises';
17
17
  import path from 'node:path';
@@ -16,8 +16,8 @@
16
16
  * pnpm mem:summarize --wu WU-1234 --dry-run # Preview without changes
17
17
  * pnpm mem:summarize --wu WU-1234 --json # Output as JSON
18
18
  *
19
- * @see {@link tools/lib/mem-summarize-core.mjs} - Core logic
20
- * @see {@link tools/__tests__/mem-summarize.test.mjs} - Tests
19
+ * @see {@link packages/@lumenflow/cli/src/lib/mem-summarize-core.ts} - Core logic
20
+ * @see {@link packages/@lumenflow/cli/src/__tests__/mem-summarize.test.ts} - Tests
21
21
  */
22
22
  import fs from 'node:fs/promises';
23
23
  import path from 'node:path';
@@ -16,8 +16,8 @@
16
16
  * pnpm mem:triage --promote mem-aaa1 --lane "Operations: Tooling"
17
17
  * pnpm mem:triage --archive mem-aaa1 --reason "Duplicate"
18
18
  *
19
- * @see {@link tools/lib/mem-triage-core.mjs} - Core logic
20
- * @see {@link tools/__tests__/mem-triage.test.mjs} - Tests
19
+ * @see {@link packages/@lumenflow/cli/src/lib/mem-triage-core.ts} - Core logic
20
+ * @see {@link packages/@lumenflow/cli/src/__tests__/mem-triage.test.ts} - Tests
21
21
  */
22
22
  import fs from 'node:fs/promises';
23
23
  import path from 'node:path';
@@ -0,0 +1,222 @@
1
+ /**
2
+ * @file merge-block.ts
3
+ * Merge block utilities for safe, idempotent config insertion (WU-1171)
4
+ *
5
+ * This module provides utilities to safely merge LumenFlow configuration
6
+ * into existing files using bounded markers (LUMENFLOW:START/END).
7
+ */
8
+ /**
9
+ * Marker comments for LumenFlow blocks
10
+ */
11
+ export const MARKERS = {
12
+ START: '<!-- LUMENFLOW:START -->',
13
+ END: '<!-- LUMENFLOW:END -->',
14
+ };
15
+ /**
16
+ * Detect the predominant line ending in content.
17
+ *
18
+ * @param content - The file content to analyze
19
+ * @returns '\r\n' for CRLF, '\n' for LF (default)
20
+ */
21
+ export function detectLineEnding(content) {
22
+ if (!content) {
23
+ return '\n';
24
+ }
25
+ const crlfCount = (content.match(/\r\n/g) || []).length;
26
+ const lfCount = (content.match(/(?<!\r)\n/g) || []).length;
27
+ // If no line endings found, default to LF
28
+ if (crlfCount === 0 && lfCount === 0) {
29
+ return '\n';
30
+ }
31
+ // Use majority line ending
32
+ return crlfCount >= lfCount ? '\r\n' : '\n';
33
+ }
34
+ /**
35
+ * Normalize line endings in content to the specified style.
36
+ *
37
+ * @param content - The content to normalize
38
+ * @param lineEnding - The target line ending
39
+ * @returns Content with normalized line endings
40
+ */
41
+ export function normalizeLineEndings(content, lineEnding) {
42
+ // First normalize to LF, then convert to target
43
+ const normalized = content.replace(/\r\n/g, '\n');
44
+ if (lineEnding === '\r\n') {
45
+ return normalized.replace(/\n/g, '\r\n');
46
+ }
47
+ return normalized;
48
+ }
49
+ /**
50
+ * Extract the LumenFlow block from content if it exists.
51
+ *
52
+ * @param content - The file content to search
53
+ * @returns Extraction result with block details
54
+ */
55
+ export function extractMergeBlock(content) {
56
+ // Use indexOf for simple string matching instead of regex to avoid DOS vulnerability
57
+ const startIndexes = [];
58
+ const endIndexes = [];
59
+ let pos = 0;
60
+ while ((pos = content.indexOf(MARKERS.START, pos)) !== -1) {
61
+ startIndexes.push(pos);
62
+ pos += MARKERS.START.length;
63
+ }
64
+ pos = 0;
65
+ while ((pos = content.indexOf(MARKERS.END, pos)) !== -1) {
66
+ endIndexes.push(pos);
67
+ pos += MARKERS.END.length;
68
+ }
69
+ // Check for malformed cases
70
+ if (startIndexes.length > 1) {
71
+ return { found: false, malformed: true, malformedReason: 'multiple-start' };
72
+ }
73
+ if (endIndexes.length > 1) {
74
+ return { found: false, malformed: true, malformedReason: 'multiple-end' };
75
+ }
76
+ const hasStart = startIndexes.length === 1;
77
+ const hasEnd = endIndexes.length === 1;
78
+ if (hasStart && !hasEnd) {
79
+ return { found: false, malformed: true, malformedReason: 'missing-end' };
80
+ }
81
+ if (!hasStart && hasEnd) {
82
+ return { found: false, malformed: true, malformedReason: 'missing-start' };
83
+ }
84
+ if (!hasStart && !hasEnd) {
85
+ return { found: false };
86
+ }
87
+ // Both markers present - extract content
88
+ const startIndex = startIndexes[0];
89
+ const endMarkerIndex = endIndexes[0];
90
+ const endIndex = endMarkerIndex + MARKERS.END.length;
91
+ // Verify END comes after START
92
+ if (endMarkerIndex <= startIndex) {
93
+ return { found: false, malformed: true, malformedReason: 'missing-end' };
94
+ }
95
+ // Extract content between markers (excluding the markers and surrounding newlines)
96
+ const afterStart = startIndex + MARKERS.START.length;
97
+ const beforeEnd = endMarkerIndex;
98
+ let blockContent = content.slice(afterStart, beforeEnd);
99
+ // Trim leading/trailing newlines from the block content
100
+ // Use simple loop to avoid regex ReDoS vulnerability
101
+ blockContent = trimNewlines(blockContent);
102
+ return {
103
+ found: true,
104
+ content: blockContent,
105
+ startIndex,
106
+ endIndex,
107
+ };
108
+ }
109
+ /**
110
+ * Insert a new merge block into content.
111
+ *
112
+ * @param originalContent - The existing file content
113
+ * @param blockContent - The content to place inside the block
114
+ * @returns The content with the new block appended
115
+ */
116
+ export function insertMergeBlock(originalContent, blockContent) {
117
+ const lineEnding = detectLineEnding(originalContent);
118
+ const normalizedBlock = normalizeLineEndings(blockContent, lineEnding);
119
+ // Ensure content ends with newlines for separation
120
+ let content = originalContent;
121
+ if (!content.endsWith(lineEnding)) {
122
+ content += lineEnding;
123
+ }
124
+ if (!content.endsWith(lineEnding + lineEnding) && content.trim().length > 0) {
125
+ content += lineEnding;
126
+ }
127
+ // Build the block
128
+ const block = [MARKERS.START, normalizedBlock, MARKERS.END, ''].join(lineEnding);
129
+ return content + block;
130
+ }
131
+ /**
132
+ * Update or insert a merge block in content.
133
+ *
134
+ * @param originalContent - The existing file content
135
+ * @param newBlockContent - The new content for the block
136
+ * @returns Result with the updated content and metadata
137
+ */
138
+ export function updateMergeBlock(originalContent, newBlockContent) {
139
+ const lineEnding = detectLineEnding(originalContent);
140
+ const extraction = extractMergeBlock(originalContent);
141
+ // If malformed, warn and append fresh block
142
+ if (extraction.malformed) {
143
+ const warning = `LumenFlow markers are malformed (${extraction.malformedReason}). Appending fresh block.`;
144
+ const result = insertMergeBlock(originalContent, newBlockContent);
145
+ return {
146
+ content: result,
147
+ updated: true,
148
+ wasInserted: true,
149
+ warning,
150
+ };
151
+ }
152
+ // If no existing block, insert new one
153
+ if (!extraction.found) {
154
+ return {
155
+ content: insertMergeBlock(originalContent, newBlockContent),
156
+ updated: true,
157
+ wasInserted: true,
158
+ };
159
+ }
160
+ // Check if content is unchanged
161
+ const normalizedNew = normalizeLineEndings(newBlockContent, lineEnding).trim();
162
+ const normalizedExisting = (extraction.content || '').trim();
163
+ if (normalizedNew === normalizedExisting) {
164
+ return {
165
+ content: originalContent,
166
+ updated: false,
167
+ unchanged: true,
168
+ };
169
+ }
170
+ // Replace existing block - at this point we know extraction.found is true
171
+ // so startIndex and endIndex are defined
172
+ const startIdx = extraction.startIndex ?? 0;
173
+ const endIdx = extraction.endIndex ?? originalContent.length;
174
+ const before = originalContent.slice(0, startIdx);
175
+ const after = originalContent.slice(endIdx);
176
+ // Build the new block with preserved line endings
177
+ const newBlock = [
178
+ MARKERS.START,
179
+ normalizeLineEndings(newBlockContent, lineEnding),
180
+ MARKERS.END,
181
+ ].join(lineEnding);
182
+ // Combine parts, ensuring proper line ending between before and block
183
+ let result = before;
184
+ if (!result.endsWith(lineEnding) && result.length > 0) {
185
+ result += lineEnding;
186
+ }
187
+ result += newBlock;
188
+ // Handle after part
189
+ if (after.trim().length > 0) {
190
+ if (!result.endsWith(lineEnding)) {
191
+ result += lineEnding;
192
+ }
193
+ result += after;
194
+ }
195
+ else if (after.includes(lineEnding)) {
196
+ // Preserve trailing newline if original had it
197
+ if (!result.endsWith(lineEnding)) {
198
+ result += lineEnding;
199
+ }
200
+ }
201
+ return {
202
+ content: result,
203
+ updated: true,
204
+ };
205
+ }
206
+ /**
207
+ * Trim leading and trailing newlines from a string.
208
+ * Uses simple loop to avoid regex ReDoS vulnerability.
209
+ */
210
+ function trimNewlines(str) {
211
+ let start = 0;
212
+ let end = str.length;
213
+ // Trim leading newlines
214
+ while (start < end && (str[start] === '\r' || str[start] === '\n')) {
215
+ start++;
216
+ }
217
+ // Trim trailing newlines
218
+ while (end > start && (str[end - 1] === '\r' || str[end - 1] === '\n')) {
219
+ end--;
220
+ }
221
+ return str.slice(start, end);
222
+ }
@@ -25,12 +25,14 @@ import { Command } from 'commander';
25
25
  import { captureMetricsSnapshot, calculateDORAMetrics, calculateFlowState, } from '@lumenflow/metrics';
26
26
  import { getGitForCwd } from '@lumenflow/core/dist/git-adapter.js';
27
27
  import { die } from '@lumenflow/core/dist/error-handler.js';
28
+ import { createWuPaths } from '@lumenflow/core/dist/wu-paths.js';
28
29
  /** Log prefix for console output */
29
30
  const LOG_PREFIX = '[metrics]';
30
31
  /** Default snapshot output path */
31
32
  const DEFAULT_OUTPUT = '.lumenflow/snapshots/metrics-latest.json';
33
+ const wuPaths = createWuPaths();
32
34
  /** WU directory relative to repo root */
33
- const WU_DIR = 'docs/04-operations/tasks/wu';
35
+ const WU_DIR = wuPaths.WU_DIR();
34
36
  /** Skip-gates audit file path */
35
37
  const SKIP_GATES_PATH = '.lumenflow/skip-gates-audit.ndjson';
36
38
  /**
@@ -424,9 +426,10 @@ async function main() {
424
426
  break;
425
427
  }
426
428
  }
427
- // Guard main() for testability
428
- import { fileURLToPath } from 'node:url';
429
- if (process.argv[1] === fileURLToPath(import.meta.url)) {
429
+ // WU-1181: Use import.meta.main instead of process.argv[1] comparison
430
+ // The old pattern fails with pnpm symlinks because process.argv[1] is the symlink
431
+ // path but import.meta.url resolves to the real path - they never match
432
+ if (import.meta.main) {
430
433
  main().catch((err) => {
431
434
  die(`Metrics command failed: ${err.message}`);
432
435
  });
@@ -305,9 +305,10 @@ async function main() {
305
305
  console.log(`${LOG_PREFIX} ✅ Snapshot written to: ${outputPath}`);
306
306
  }
307
307
  }
308
- // Guard main() for testability (WU-1366)
309
- import { fileURLToPath } from 'node:url';
310
- if (process.argv[1] === fileURLToPath(import.meta.url)) {
308
+ // WU-1181: Use import.meta.main instead of process.argv[1] comparison
309
+ // The old pattern fails with pnpm symlinks because process.argv[1] is the symlink
310
+ // path but import.meta.url resolves to the real path - they never match
311
+ if (import.meta.main) {
311
312
  main().catch((err) => {
312
313
  die(`Metrics snapshot failed: ${err.message}`);
313
314
  });
@@ -11,7 +11,7 @@
11
11
  */
12
12
  import { Command } from 'commander';
13
13
  import chalk from 'chalk';
14
- import { loadInitiativeWUs, loadMultipleInitiatives, buildExecutionPlan, formatExecutionPlan, calculateProgress, formatProgress, buildCheckpointWave, formatCheckpointOutput, validateCheckpointFlags, resolveCheckpointMode, LOG_PREFIX, } from '@lumenflow/initiatives';
14
+ import { loadInitiativeWUs, loadMultipleInitiatives, buildExecutionPlanAsync, formatExecutionPlan, formatExecutionPlanWithEmbeddedSpawns, calculateProgress, formatProgress, buildCheckpointWave, formatCheckpointOutput, validateCheckpointFlags, resolveCheckpointModeAsync, LOG_PREFIX, } from '@lumenflow/initiatives';
15
15
  import { EXIT_CODES } from '@lumenflow/core/dist/wu-constants.js';
16
16
  const program = new Command()
17
17
  .name('orchestrate-initiative')
@@ -61,7 +61,7 @@ const program = new Command()
61
61
  if (progressOnly) {
62
62
  return;
63
63
  }
64
- const checkpointDecision = resolveCheckpointMode({ checkpointPerWave, noCheckpoint, dryRun }, wus);
64
+ const checkpointDecision = await resolveCheckpointModeAsync({ checkpointPerWave, noCheckpoint, dryRun }, wus);
65
65
  if (checkpointDecision.enabled) {
66
66
  if (initIds.length > 1) {
67
67
  console.error(chalk.red(`${LOG_PREFIX} Error: Checkpoint mode only supports single initiative`));
@@ -76,7 +76,7 @@ const program = new Command()
76
76
  return;
77
77
  }
78
78
  console.log(chalk.cyan(`${LOG_PREFIX} Building execution plan...`));
79
- const plan = buildExecutionPlan(wus);
79
+ const plan = await buildExecutionPlanAsync(wus);
80
80
  if (plan.waves.length === 0) {
81
81
  console.log(chalk.green(`${LOG_PREFIX} All WUs are complete! Nothing to execute.`));
82
82
  return;
@@ -89,8 +89,14 @@ const program = new Command()
89
89
  console.log(chalk.cyan('To execute this plan, remove the --dry-run flag.'));
90
90
  return;
91
91
  }
92
+ // WU-1202: Output spawn XML for actual execution (not dry-run)
93
+ // formatExecutionPlan only shows the plan structure, not spawn commands
94
+ // formatExecutionPlanWithEmbeddedSpawns includes Task XML for spawning agents
95
+ console.log('');
96
+ console.log(chalk.bold('Spawn Commands:'));
97
+ console.log(formatExecutionPlanWithEmbeddedSpawns(plan));
92
98
  console.log(chalk.green(`${LOG_PREFIX} Execution plan output complete.`));
93
- console.log(chalk.cyan('Copy the spawn commands above to execute.'));
99
+ console.log(chalk.cyan('Copy the spawn XML above to execute agents.'));
94
100
  }
95
101
  catch (error) {
96
102
  console.error(chalk.red(`${LOG_PREFIX} Error: ${error.message}`));