@lumenflow/cli 2.6.0 → 2.8.0

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 (88) hide show
  1. package/README.md +120 -105
  2. package/dist/__tests__/agent-spawn-coordination.test.js +451 -0
  3. package/dist/__tests__/commands/integrate.test.js +165 -0
  4. package/dist/__tests__/gates-config.test.js +0 -1
  5. package/dist/__tests__/hooks/enforcement.test.js +279 -0
  6. package/dist/__tests__/init-greenfield.test.js +247 -0
  7. package/dist/__tests__/init-quick-ref.test.js +0 -1
  8. package/dist/__tests__/init-template-portability.test.js +0 -1
  9. package/dist/__tests__/init.test.js +27 -0
  10. package/dist/__tests__/initiative-e2e.test.js +442 -0
  11. package/dist/__tests__/initiative-plan-replacement.test.js +0 -1
  12. package/dist/__tests__/memory-integration.test.js +333 -0
  13. package/dist/__tests__/release.test.js +1 -1
  14. package/dist/__tests__/safe-git.test.js +0 -1
  15. package/dist/__tests__/state-doctor.test.js +54 -0
  16. package/dist/__tests__/sync-templates.test.js +255 -0
  17. package/dist/__tests__/wu-create-required-fields.test.js +121 -0
  18. package/dist/__tests__/wu-done-auto-cleanup.test.js +135 -0
  19. package/dist/__tests__/wu-lifecycle-integration.test.js +388 -0
  20. package/dist/__tests__/wu-proto.test.js +97 -0
  21. package/dist/backlog-prune.js +0 -1
  22. package/dist/cli-entry-point.js +0 -1
  23. package/dist/commands/integrate.js +229 -0
  24. package/dist/docs-sync.js +46 -0
  25. package/dist/doctor.js +0 -2
  26. package/dist/gates.js +0 -7
  27. package/dist/hooks/enforcement-checks.js +209 -0
  28. package/dist/hooks/enforcement-generator.js +365 -0
  29. package/dist/hooks/enforcement-sync.js +243 -0
  30. package/dist/hooks/index.js +7 -0
  31. package/dist/init.js +266 -11
  32. package/dist/initiative-add-wu.js +0 -2
  33. package/dist/initiative-create.js +0 -3
  34. package/dist/initiative-edit.js +0 -5
  35. package/dist/initiative-plan.js +0 -1
  36. package/dist/initiative-remove-wu.js +0 -2
  37. package/dist/lane-health.js +0 -2
  38. package/dist/lane-suggest.js +0 -1
  39. package/dist/mem-checkpoint.js +0 -2
  40. package/dist/mem-cleanup.js +0 -2
  41. package/dist/mem-context.js +0 -3
  42. package/dist/mem-create.js +0 -2
  43. package/dist/mem-delete.js +0 -3
  44. package/dist/mem-inbox.js +0 -2
  45. package/dist/mem-index.js +0 -1
  46. package/dist/mem-init.js +0 -2
  47. package/dist/mem-profile.js +0 -1
  48. package/dist/mem-promote.js +0 -1
  49. package/dist/mem-ready.js +0 -2
  50. package/dist/mem-signal.js +0 -2
  51. package/dist/mem-start.js +0 -2
  52. package/dist/mem-summarize.js +0 -2
  53. package/dist/metrics-cli.js +1 -1
  54. package/dist/metrics-snapshot.js +1 -1
  55. package/dist/onboarding-smoke-test.js +0 -5
  56. package/dist/orchestrate-init-status.js +0 -1
  57. package/dist/orchestrate-initiative.js +0 -1
  58. package/dist/orchestrate-monitor.js +0 -1
  59. package/dist/plan-create.js +0 -2
  60. package/dist/plan-edit.js +0 -2
  61. package/dist/plan-link.js +0 -2
  62. package/dist/plan-promote.js +0 -2
  63. package/dist/signal-cleanup.js +0 -4
  64. package/dist/state-bootstrap.js +0 -1
  65. package/dist/state-cleanup.js +0 -4
  66. package/dist/state-doctor-fix.js +5 -8
  67. package/dist/state-doctor.js +0 -11
  68. package/dist/sync-templates.js +188 -34
  69. package/dist/wu-block.js +100 -48
  70. package/dist/wu-claim.js +1 -22
  71. package/dist/wu-cleanup.js +0 -1
  72. package/dist/wu-create.js +0 -2
  73. package/dist/wu-done-auto-cleanup.js +139 -0
  74. package/dist/wu-done.js +11 -4
  75. package/dist/wu-edit.js +0 -12
  76. package/dist/wu-preflight.js +0 -1
  77. package/dist/wu-prep.js +0 -1
  78. package/dist/wu-proto.js +329 -0
  79. package/dist/wu-spawn.js +0 -3
  80. package/dist/wu-unblock.js +0 -2
  81. package/dist/wu-validate.js +0 -1
  82. package/package.json +9 -7
  83. package/templates/core/.husky/pre-commit.template +93 -0
  84. package/templates/core/ai/onboarding/quick-ref-commands.md.template +27 -0
  85. package/templates/core/ai/onboarding/rapid-prototyping.md +143 -0
  86. package/templates/core/ai/onboarding/starting-prompt.md.template +3 -3
  87. package/templates/vendors/claude/.claude/CLAUDE.md.template +25 -0
  88. package/templates/vendors/claude/.claude/hooks/enforce-worktree.sh +135 -0
package/dist/wu-block.js CHANGED
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- /* eslint-disable no-console -- CLI tool requires console output */
3
2
  /**
4
3
  * WU Block Helper
5
4
  *
@@ -29,7 +28,7 @@ import { readWU, writeWU, appendNote } from '@lumenflow/core/dist/wu-yaml.js';
29
28
  import { BRANCHES, REMOTES, WU_STATUS, STATUS_SECTIONS, PATTERNS, LOG_PREFIX, FILE_SYSTEM, EXIT_CODES, STRING_LITERALS, MICRO_WORKTREE_OPERATIONS, } from '@lumenflow/core/dist/wu-constants.js';
30
29
  import { ensureOnMain } from '@lumenflow/core/dist/wu-helpers.js';
31
30
  import { ensureStaged } from '@lumenflow/core/dist/git-staged-validator.js';
32
- import { withMicroWorktree } from '@lumenflow/core/dist/micro-worktree.js';
31
+ import { withMicroWorktree, LUMENFLOW_WU_TOOL_ENV } from '@lumenflow/core/dist/micro-worktree.js';
33
32
  import { WUStateStore } from '@lumenflow/core/dist/wu-state-store.js';
34
33
  // WU-1603: Atomic lane locking - release lock when WU is blocked
35
34
  import { releaseLaneLock } from '@lumenflow/core/dist/lane-lock.js';
@@ -38,6 +37,30 @@ import { getLockPolicyForLane } from '@lumenflow/core/dist/lane-checker.js';
38
37
  // ensureOnMain() moved to wu-helpers.ts (WU-1256)
39
38
  // ensureStaged() moved to git-staged-validator.ts (WU-1341)
40
39
  // defaultWorktreeFrom() moved to wu-paths.ts (WU-1341)
40
+ /**
41
+ * WU-1365: Execute a function with LUMENFLOW_WU_TOOL set, restoring afterwards
42
+ *
43
+ * Sets the LUMENFLOW_WU_TOOL env var to allow pre-push hook bypass, then
44
+ * restores the original value (or deletes it) after execution completes.
45
+ *
46
+ * @param toolName - Value to set for LUMENFLOW_WU_TOOL
47
+ * @param fn - Async function to execute
48
+ */
49
+ async function withWuToolEnv(toolName, fn) {
50
+ const previousWuTool = process.env[LUMENFLOW_WU_TOOL_ENV];
51
+ process.env[LUMENFLOW_WU_TOOL_ENV] = toolName;
52
+ try {
53
+ return await fn();
54
+ }
55
+ finally {
56
+ if (previousWuTool === undefined) {
57
+ Reflect.deleteProperty(process.env, LUMENFLOW_WU_TOOL_ENV);
58
+ }
59
+ else {
60
+ process.env[LUMENFLOW_WU_TOOL_ENV] = previousWuTool;
61
+ }
62
+ }
63
+ }
41
64
  /**
42
65
  * Remove WU entry from in-progress section of lines array
43
66
  */
@@ -47,7 +70,6 @@ function removeFromInProgressSection(lines, inProgIdx, rel, id) {
47
70
  let endIdx = lines.slice(inProgIdx + 1).findIndex((l) => l.startsWith('## '));
48
71
  endIdx = endIdx === -1 ? lines.length : inProgIdx + 1 + endIdx;
49
72
  for (let i = inProgIdx + 1; i < endIdx; i++) {
50
- // eslint-disable-next-line security/detect-object-injection -- array index loop
51
73
  if (lines[i] && (lines[i].includes(rel) || lines[i].includes(`[${id}`))) {
52
74
  lines.splice(i, 1);
53
75
  endIdx--;
@@ -58,6 +80,31 @@ function removeFromInProgressSection(lines, inProgIdx, rel, id) {
58
80
  if (section.length === 0)
59
81
  lines.splice(endIdx, 0, '', '(No items currently in progress)', '');
60
82
  }
83
+ /**
84
+ * WU-1365: Create missing blocked section in status.md
85
+ *
86
+ * Extracts this logic to reduce cognitive complexity of moveFromInProgressToBlocked.
87
+ *
88
+ * @param lines - Array of lines from status.md
89
+ * @param inProgIdx - Index of "## In Progress" header (-1 if not found)
90
+ * @returns Index of the blocked section header after creation
91
+ */
92
+ function createMissingBlockedSection(lines, inProgIdx) {
93
+ console.log(`${LOG_PREFIX.BLOCK} Creating missing "${STATUS_SECTIONS.BLOCKED}" section in status.md`);
94
+ // Find a good insertion point - after in_progress section or at end
95
+ const insertIdx = inProgIdx !== -1 ? inProgIdx + 1 : lines.length;
96
+ // Skip to end of in_progress section content if it exists
97
+ let insertPoint = insertIdx;
98
+ if (inProgIdx !== -1) {
99
+ // Find where the next section starts
100
+ const nextSectionIdx = lines.slice(inProgIdx + 1).findIndex((l) => l.startsWith('## '));
101
+ insertPoint = nextSectionIdx === -1 ? lines.length : inProgIdx + 1 + nextSectionIdx;
102
+ }
103
+ // Insert the blocked section
104
+ lines.splice(insertPoint, 0, '', STATUS_SECTIONS.BLOCKED, '');
105
+ // Return the index of the newly created section header
106
+ return insertPoint + 1;
107
+ }
61
108
  async function moveFromInProgressToBlocked(statusPath, id, title, reason) {
62
109
  // Check file exists
63
110
  const fileExists = await access(statusPath)
@@ -66,23 +113,25 @@ async function moveFromInProgressToBlocked(statusPath, id, title, reason) {
66
113
  if (!fileExists)
67
114
  die(`Missing ${statusPath}`);
68
115
  const rel = `wu/${id}.yaml`;
69
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool validates status file
70
116
  const content = await readFile(statusPath, { encoding: FILE_SYSTEM.UTF8 });
71
117
  const lines = content.split(/\r?\n/);
72
118
  const findHeader = (h) => lines.findIndex((l) => l.trim().toLowerCase() === h.toLowerCase());
73
119
  const inProgIdx = findHeader(STATUS_SECTIONS.IN_PROGRESS);
74
- const blockedIdx = findHeader(STATUS_SECTIONS.BLOCKED);
75
- if (blockedIdx === -1)
76
- die(`Could not find "${STATUS_SECTIONS.BLOCKED}" in status.md`);
120
+ let blockedIdx = findHeader(STATUS_SECTIONS.BLOCKED);
121
+ // WU-1365: Handle missing blocked section gracefully by creating it
122
+ if (blockedIdx === -1) {
123
+ blockedIdx = createMissingBlockedSection(lines, inProgIdx);
124
+ }
77
125
  removeFromInProgressSection(lines, inProgIdx, rel, id);
78
126
  // Add bullet to blocked
79
127
  const reasonSuffix = reason ? ` — ${reason}` : '';
80
128
  const bullet = `- [${id} — ${title}](${rel})${reasonSuffix}`;
129
+ // Recalculate blockedIdx after removeFromInProgressSection may have changed line positions
130
+ blockedIdx = findHeader(STATUS_SECTIONS.BLOCKED);
81
131
  const sectionStart = blockedIdx + 1;
82
132
  if (lines.slice(sectionStart).some((l) => l.includes(rel)))
83
133
  return; // already listed
84
134
  lines.splice(sectionStart, 0, '', bullet);
85
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool writes status file
86
135
  await writeFile(statusPath, lines.join(STRING_LITERALS.NEWLINE), {
87
136
  encoding: FILE_SYSTEM.UTF8,
88
137
  });
@@ -169,47 +218,50 @@ async function main() {
169
218
  const baseMsg = `wu(${id.toLowerCase()}): block`;
170
219
  const commitMsg = args.reason ? `${baseMsg} — ${args.reason}` : baseMsg;
171
220
  if (!args.noAuto) {
172
- // Use micro-worktree pattern to avoid pre-commit hook blocking commits to main
173
- await withMicroWorktree({
174
- operation: MICRO_WORKTREE_OPERATIONS.WU_BLOCK,
175
- id,
176
- logPrefix: LOG_PREFIX.BLOCK,
177
- pushOnly: true, // Push directly to origin/main without touching local main
178
- execute: async ({ worktreePath }) => {
179
- // Build paths relative to micro-worktree
180
- const microWUPath = path.join(worktreePath, WU_PATHS.WU(id));
181
- const microStatusPath = path.join(worktreePath, WU_PATHS.STATUS());
182
- const microBacklogPath = path.join(worktreePath, WU_PATHS.BACKLOG());
183
- // Update WU YAML in micro-worktree
184
- const microDoc = readWU(microWUPath, id);
185
- microDoc.status = WU_STATUS.BLOCKED;
186
- const noteLine = args.reason
187
- ? `Blocked (${todayISO()}): ${args.reason}`
188
- : `Blocked (${todayISO()})`;
189
- appendNote(microDoc, noteLine);
190
- writeWU(microWUPath, microDoc);
191
- // Update status.md in micro-worktree
192
- await moveFromInProgressToBlocked(microStatusPath, id, title, args.reason);
193
- // Update backlog.md in micro-worktree (WU-1574: regenerate from state store)
194
- await regenerateBacklogFromState(microBacklogPath);
195
- // Append block event to WUStateStore (WU-1573)
196
- const stateDir = path.join(worktreePath, '.lumenflow', 'state');
197
- const store = new WUStateStore(stateDir);
198
- await store.load();
199
- await store.block(id, args.reason || 'No reason provided');
200
- return {
201
- commitMessage: commitMsg,
202
- files: [
203
- WU_PATHS.WU(id),
204
- WU_PATHS.STATUS(),
205
- WU_PATHS.BACKLOG(),
206
- '.lumenflow/state/wu-events.jsonl',
207
- ],
208
- };
209
- },
221
+ // WU-1365: Set LUMENFLOW_WU_TOOL to allow pre-push hook bypass for micro-worktree pushes
222
+ await withWuToolEnv(MICRO_WORKTREE_OPERATIONS.WU_BLOCK, async () => {
223
+ // Use micro-worktree pattern to avoid pre-commit hook blocking commits to main
224
+ await withMicroWorktree({
225
+ operation: MICRO_WORKTREE_OPERATIONS.WU_BLOCK,
226
+ id,
227
+ logPrefix: LOG_PREFIX.BLOCK,
228
+ pushOnly: true, // Push directly to origin/main without touching local main
229
+ execute: async ({ worktreePath }) => {
230
+ // Build paths relative to micro-worktree
231
+ const microWUPath = path.join(worktreePath, WU_PATHS.WU(id));
232
+ const microStatusPath = path.join(worktreePath, WU_PATHS.STATUS());
233
+ const microBacklogPath = path.join(worktreePath, WU_PATHS.BACKLOG());
234
+ // Update WU YAML in micro-worktree
235
+ const microDoc = readWU(microWUPath, id);
236
+ microDoc.status = WU_STATUS.BLOCKED;
237
+ const noteLine = args.reason
238
+ ? `Blocked (${todayISO()}): ${args.reason}`
239
+ : `Blocked (${todayISO()})`;
240
+ appendNote(microDoc, noteLine);
241
+ writeWU(microWUPath, microDoc);
242
+ // Update status.md in micro-worktree
243
+ await moveFromInProgressToBlocked(microStatusPath, id, title, args.reason);
244
+ // Update backlog.md in micro-worktree (WU-1574: regenerate from state store)
245
+ await regenerateBacklogFromState(microBacklogPath);
246
+ // Append block event to WUStateStore (WU-1573)
247
+ const stateDir = path.join(worktreePath, '.lumenflow', 'state');
248
+ const store = new WUStateStore(stateDir);
249
+ await store.load();
250
+ await store.block(id, args.reason || 'No reason provided');
251
+ return {
252
+ commitMessage: commitMsg,
253
+ files: [
254
+ WU_PATHS.WU(id),
255
+ WU_PATHS.STATUS(),
256
+ WU_PATHS.BACKLOG(),
257
+ '.lumenflow/state/wu-events.jsonl',
258
+ ],
259
+ };
260
+ },
261
+ });
262
+ // Fetch to update local main tracking
263
+ await getGitForCwd().fetch(REMOTES.ORIGIN, BRANCHES.MAIN);
210
264
  });
211
- // Fetch to update local main tracking
212
- await getGitForCwd().fetch(REMOTES.ORIGIN, BRANCHES.MAIN);
213
265
  }
214
266
  else {
215
267
  // Manual mode: expect files already staged
package/dist/wu-claim.js CHANGED
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- /* eslint-disable no-console -- CLI tool requires console output */
3
2
  /**
4
3
  * WU Claim Helper
5
4
  *
@@ -27,9 +26,7 @@ import { checkLaneFree, validateLaneFormat, checkWipJustification, } from '@lume
27
26
  import { acquireLaneLock, releaseLaneLock, checkLaneLock, forceRemoveStaleLock, } from '@lumenflow/core/dist/lane-lock.js';
28
27
  // WU-1825: Import from unified code-path-validator (consolidates 3 validators)
29
28
  // WU-1213: Using deprecated sync API - async validate() requires larger refactor (separate WU)
30
- import {
31
- // eslint-disable-next-line sonarjs/deprecation -- sync API required for current architecture
32
- validateLaneCodePaths, logLaneValidationWarnings, } from '@lumenflow/core/dist/code-path-validator.js';
29
+ import { validateLaneCodePaths, logLaneValidationWarnings, } from '@lumenflow/core/dist/code-path-validator.js';
33
30
  // WU-1574: parseBacklogFrontmatter/getSectionHeadings removed - state store replaces backlog parsing
34
31
  import { detectConflicts } from '@lumenflow/core/dist/code-paths-overlap.js';
35
32
  import { getGitForCwd, createGitForPath } from '@lumenflow/core/dist/git-adapter.js';
@@ -86,7 +83,6 @@ const PREFIX = LOG_PREFIX.CLAIM;
86
83
  */
87
84
  function preflightValidateWU(WU_PATH, id) {
88
85
  // Check file exists
89
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool validates WU files
90
86
  if (!existsSync(WU_PATH)) {
91
87
  die(`WU file not found: ${WU_PATH}\n\n` +
92
88
  `Cannot claim a WU that doesn't exist.\n\n` +
@@ -96,7 +92,6 @@ function preflightValidateWU(WU_PATH, id) {
96
92
  ` 3. Check if the WU file was moved or deleted`);
97
93
  }
98
94
  // Parse and validate YAML structure
99
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool validates WU files
100
95
  const text = readFileSync(WU_PATH, { encoding: FILE_SYSTEM.UTF8 });
101
96
  let doc;
102
97
  try {
@@ -189,7 +184,6 @@ async function updateWUYaml(WU_PATH, id, lane, claimedMode = 'worktree', worktre
189
184
  // Read file
190
185
  let text;
191
186
  try {
192
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool validates WU files
193
187
  text = await readFile(WU_PATH, { encoding: FILE_SYSTEM.UTF8 });
194
188
  }
195
189
  catch (e) {
@@ -266,7 +260,6 @@ async function updateWUYaml(WU_PATH, id, lane, claimedMode = 'worktree', worktre
266
260
  // WU-1352: Use centralized stringify for consistent output
267
261
  const out = stringifyYAML(doc);
268
262
  // Write file
269
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool writes WU files
270
263
  await writeFile(WU_PATH, out, { encoding: FILE_SYSTEM.UTF8 });
271
264
  // WU-1211: Return both title and initiative for status progression check
272
265
  return { title: doc.title || '', initiative: doc.initiative || null };
@@ -326,7 +319,6 @@ async function addOrReplaceInProgressStatus(statusPath, id, title) {
326
319
  const rel = `wu/${id}.yaml`;
327
320
  const bullet = `- [${id} — ${title}](${rel})`;
328
321
  // Read file
329
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool validates status file
330
322
  const content = await readFile(statusPath, { encoding: FILE_SYSTEM.UTF8 });
331
323
  const lines = content.split(STRING_LITERALS.NEWLINE);
332
324
  const findHeader = (h) => lines.findIndex((l) => l.trim().toLowerCase() === h.toLowerCase());
@@ -344,7 +336,6 @@ async function addOrReplaceInProgressStatus(statusPath, id, title) {
344
336
  return; // already listed
345
337
  // Remove "No items" marker if present
346
338
  for (let i = startIdx + 1; i < endIdx; i++) {
347
- // eslint-disable-next-line security/detect-object-injection -- array index loop
348
339
  if (lines[i] && lines[i].includes('No items currently in progress')) {
349
340
  lines.splice(i, 1);
350
341
  endIdx--;
@@ -354,7 +345,6 @@ async function addOrReplaceInProgressStatus(statusPath, id, title) {
354
345
  // Insert bullet right after header
355
346
  lines.splice(startIdx + 1, 0, '', bullet);
356
347
  // Write file
357
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool writes status file
358
348
  await writeFile(statusPath, lines.join(STRING_LITERALS.NEWLINE), {
359
349
  encoding: FILE_SYSTEM.UTF8,
360
350
  });
@@ -444,10 +434,8 @@ async function applyStagedChangesToMicroWorktree(worktreePath, stagedChanges) {
444
434
  continue;
445
435
  }
446
436
  const sourcePath = path.join(process.cwd(), filePath);
447
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool applies staged files
448
437
  const contents = await readFile(sourcePath, { encoding: FILE_SYSTEM.UTF8 });
449
438
  await mkdir(path.dirname(targetPath), { recursive: true });
450
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool applies staged files
451
439
  await writeFile(targetPath, contents, { encoding: FILE_SYSTEM.UTF8 });
452
440
  }
453
441
  }
@@ -520,11 +508,9 @@ async function readWUTitle(id) {
520
508
  return null;
521
509
  }
522
510
  // Read file
523
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool validates WU files
524
511
  const text = await readFile(p, { encoding: FILE_SYSTEM.UTF8 });
525
512
  // Match title field - use RegExp.exec for sonarjs/prefer-regexp-exec compliance
526
513
  // Regex is safe: runs on trusted WU YAML files with bounded input
527
- // eslint-disable-next-line sonarjs/slow-regex -- Bounded input from WU YAML files
528
514
  const titlePattern = /^title:\s*"?([^"\n]+)"?$/m;
529
515
  const m = titlePattern.exec(text);
530
516
  return m ? m[1] : null;
@@ -546,7 +532,6 @@ async function checkExistingBranchOnlyWU(statusPath, currentWU) {
546
532
  return { hasBranchOnly: false, existingWU: null };
547
533
  }
548
534
  // Read file
549
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool validates status file
550
535
  const content = await readFile(statusPath, { encoding: FILE_SYSTEM.UTF8 });
551
536
  const lines = content.split(STRING_LITERALS.NEWLINE);
552
537
  // Find "In Progress" section
@@ -581,7 +566,6 @@ async function checkExistingBranchOnlyWU(statusPath, currentWU) {
581
566
  }
582
567
  try {
583
568
  // Read file
584
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool validates WU files
585
569
  const text = await readFile(wuPath, { encoding: FILE_SYSTEM.UTF8 });
586
570
  const doc = parseYAML(text);
587
571
  if (doc && doc.claimed_mode === CLAIMED_MODES.BRANCH_ONLY) {
@@ -705,10 +689,8 @@ function handleLaneOccupancy(laneCheck, lane, id, force) {
705
689
  * Handle code path overlap detection (WU-901)
706
690
  */
707
691
  function handleCodePathOverlap(WU_PATH, STATUS_PATH, id, args) {
708
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool validates WU files
709
692
  if (!existsSync(WU_PATH))
710
693
  return;
711
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool validates WU files
712
694
  const wuContent = readFileSync(WU_PATH, { encoding: FILE_SYSTEM.UTF8 });
713
695
  const wuDoc = parseYAML(wuContent);
714
696
  const codePaths = wuDoc.code_paths || [];
@@ -846,7 +828,6 @@ async function claimBranchOnlyMode(ctx) {
846
828
  console.log(`\n${PREFIX} For sub-agent execution:`);
847
829
  console.log(` /wu-prompt ${id} (generates full context prompt)`);
848
830
  // Emit mandatory agent advisory based on code_paths (WU-1324)
849
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool validates WU files
850
831
  const wuContent = await readFile(WU_PATH, { encoding: FILE_SYSTEM.UTF8 });
851
832
  const wuDoc = parseYAML(wuContent);
852
833
  const codePaths = wuDoc.code_paths || [];
@@ -1008,7 +989,6 @@ async function claimWorktreeMode(ctx) {
1008
989
  // Emit mandatory agent advisory based on code_paths (WU-1324)
1009
990
  // Read from worktree since that's where the updated YAML is
1010
991
  const wtWUPathForAdvisory = path.join(worktreePath, WU_PATH);
1011
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool validates WU files
1012
992
  const wuContent = await readFile(wtWUPathForAdvisory, {
1013
993
  encoding: FILE_SYSTEM.UTF8,
1014
994
  });
@@ -1226,7 +1206,6 @@ async function main() {
1226
1206
  console.warn(`${PREFIX} ${wipJustificationCheck.warning}`);
1227
1207
  }
1228
1208
  // WU-1372: Lane-to-code_paths consistency check (advisory only, never blocks)
1229
- // eslint-disable-next-line sonarjs/deprecation -- sync API required for current architecture
1230
1209
  const laneValidation = validateLaneCodePaths(doc, args.lane);
1231
1210
  logLaneValidationWarnings(laneValidation, PREFIX);
1232
1211
  // WU-1361: YAML schema validation at claim time
@@ -29,7 +29,6 @@ import { isGhCliAvailable } from '@lumenflow/core/dist/wu-done-pr.js';
29
29
  import { BOX, CLEANUP_GUARD, EXIT_CODES, FILE_SYSTEM, LOG_PREFIX, REMOTES, STRING_LITERALS, WU_STATUS, } from '@lumenflow/core/dist/wu-constants.js';
30
30
  // WU-2278: Import ownership validation for cross-agent protection
31
31
  import { validateWorktreeOwnership } from '@lumenflow/core/dist/worktree-ownership.js';
32
- /* eslint-disable security/detect-non-literal-fs-filename */
33
32
  const CLEANUP_OPTIONS = {
34
33
  artifacts: {
35
34
  name: 'artifacts',
package/dist/wu-create.js CHANGED
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- /* eslint-disable no-console -- CLI tool requires console output */
3
2
  /**
4
3
  * WU Create Helper (WU-1262, WU-1439)
5
4
  *
@@ -491,7 +490,6 @@ async function getDefaultAssignedTo() {
491
490
  return '';
492
491
  }
493
492
  }
494
- // eslint-disable-next-line sonarjs/cognitive-complexity -- main() orchestrates multi-step WU creation workflow
495
493
  async function main() {
496
494
  const args = createWUParser({
497
495
  name: 'wu-create',
@@ -0,0 +1,139 @@
1
+ /**
2
+ * WU-1366: Auto cleanup after wu:done success
3
+ *
4
+ * Provides functions to run state cleanup automatically after successful wu:done.
5
+ * Cleanup is non-fatal: errors are logged but do not block completion.
6
+ *
7
+ * The cleanup.trigger config option controls when cleanup runs:
8
+ * - 'on_done': Run after wu:done success (default)
9
+ * - 'on_init': Run during lumenflow init
10
+ * - 'manual': Only run via pnpm state:cleanup
11
+ *
12
+ * @see {@link packages/@lumenflow/core/src/state-cleanup-core.ts} - Core cleanup orchestration
13
+ * @see {@link packages/@lumenflow/core/src/lumenflow-config-schema.ts} - CleanupConfigSchema
14
+ */
15
+ import { getConfig } from '@lumenflow/core/dist/lumenflow-config.js';
16
+ import { cleanupState } from '@lumenflow/core/dist/state-cleanup-core.js';
17
+ import { cleanupSignals } from '@lumenflow/memory/dist/signal-cleanup-core.js';
18
+ import { cleanupMemory } from '@lumenflow/memory/dist/mem-cleanup-core.js';
19
+ import { archiveWuEvents } from '@lumenflow/core/dist/wu-events-cleanup.js';
20
+ import fg from 'fast-glob';
21
+ import { readFile } from 'node:fs/promises';
22
+ import { parse as parseYaml } from 'yaml';
23
+ import path from 'node:path';
24
+ import { LOG_PREFIX, EMOJI } from '@lumenflow/core/dist/wu-constants.js';
25
+ /**
26
+ * Active WU statuses that should protect signals
27
+ */
28
+ const ACTIVE_WU_STATUSES = ['in_progress', 'blocked'];
29
+ /**
30
+ * Get active WU IDs (in_progress or blocked) by scanning WU YAML files.
31
+ *
32
+ * @param baseDir - Base directory
33
+ * @returns Set of active WU IDs
34
+ */
35
+ async function getActiveWuIds(baseDir) {
36
+ const activeIds = new Set();
37
+ try {
38
+ const config = getConfig({ projectRoot: baseDir });
39
+ const wuDir = path.join(baseDir, config.directories.wuDir);
40
+ // Find all WU YAML files
41
+ const wuFiles = await fg('WU-*.yaml', { cwd: wuDir });
42
+ for (const file of wuFiles) {
43
+ try {
44
+ const filePath = path.join(wuDir, file);
45
+ const content = await readFile(filePath, 'utf-8');
46
+ const wu = parseYaml(content);
47
+ if (wu.id && wu.status && ACTIVE_WU_STATUSES.includes(wu.status)) {
48
+ activeIds.add(wu.id);
49
+ }
50
+ }
51
+ catch {
52
+ // Skip files that fail to parse
53
+ continue;
54
+ }
55
+ }
56
+ }
57
+ catch {
58
+ // If we can't read WU files, return empty set (safer to remove nothing)
59
+ }
60
+ return activeIds;
61
+ }
62
+ /**
63
+ * Check if auto cleanup should run based on config.
64
+ *
65
+ * @returns true if cleanup.trigger is 'on_done' or not set (default)
66
+ */
67
+ export function shouldRunAutoCleanup() {
68
+ try {
69
+ const config = getConfig();
70
+ const trigger = config.cleanup?.trigger;
71
+ // Default to 'on_done' if not set
72
+ if (!trigger) {
73
+ return true;
74
+ }
75
+ return trigger === 'on_done';
76
+ }
77
+ catch {
78
+ // If config can't be loaded, default to running cleanup
79
+ return true;
80
+ }
81
+ }
82
+ /**
83
+ * Run state cleanup automatically after wu:done success.
84
+ *
85
+ * This function is non-fatal: errors are logged as warnings but do not throw.
86
+ * Cleanup respects the config.cleanup.trigger setting.
87
+ *
88
+ * @param baseDir - Base directory for cleanup operations
89
+ * @returns Promise that resolves when cleanup completes (or is skipped)
90
+ */
91
+ export async function runAutoCleanupAfterDone(baseDir) {
92
+ // Check if cleanup should run
93
+ if (!shouldRunAutoCleanup()) {
94
+ return;
95
+ }
96
+ try {
97
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.INFO} Running auto state cleanup...`);
98
+ const result = await cleanupState(baseDir, {
99
+ dryRun: false,
100
+ // Inject real cleanup functions
101
+ cleanupSignals: async (dir, opts) => cleanupSignals(dir, {
102
+ dryRun: opts.dryRun,
103
+ getActiveWuIds: () => getActiveWuIds(dir),
104
+ }),
105
+ cleanupMemory: async (dir, opts) => cleanupMemory(dir, {
106
+ dryRun: opts.dryRun,
107
+ }),
108
+ archiveEvents: async (dir, opts) => archiveWuEvents(dir, {
109
+ dryRun: opts.dryRun,
110
+ }),
111
+ });
112
+ if (result.success) {
113
+ const typesStr = result.summary.typesExecuted.join(', ');
114
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} State cleanup complete: ` +
115
+ `${formatBytes(result.summary.totalBytesFreed)} freed [${typesStr}]`);
116
+ }
117
+ else {
118
+ // Partial success - some cleanups failed
119
+ const errorMsgs = result.errors.map((e) => `${e.type}: ${e.message}`).join(', ');
120
+ console.warn(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} State cleanup partial: ${errorMsgs}`);
121
+ }
122
+ }
123
+ catch (err) {
124
+ // Non-fatal: log warning but don't throw
125
+ const message = err instanceof Error ? err.message : String(err);
126
+ console.warn(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Could not run auto state cleanup: ${message}`);
127
+ }
128
+ }
129
+ /**
130
+ * Format bytes as human-readable string
131
+ */
132
+ function formatBytes(bytes) {
133
+ const BYTES_PER_KB = 1024;
134
+ if (bytes < BYTES_PER_KB) {
135
+ return `${bytes} B`;
136
+ }
137
+ const kb = (bytes / BYTES_PER_KB).toFixed(1);
138
+ return `${kb} KB`;
139
+ }
package/dist/wu-done.js CHANGED
@@ -95,6 +95,8 @@ import { SpawnStatus } from '@lumenflow/core/dist/spawn-registry-schema.js';
95
95
  // WU-2022: Feature accessibility validation (blocking)
96
96
  import { validateExposure, validateFeatureAccessibility, } from '@lumenflow/core/dist/wu-validation.js';
97
97
  import { ensureCleanWorktree } from './wu-done-check.js';
98
+ // WU-1366: Auto cleanup after wu:done success
99
+ import { runAutoCleanupAfterDone } from './wu-done-auto-cleanup.js';
98
100
  // WU-1588: Memory layer constants
99
101
  const MEMORY_SIGNAL_TYPES = {
100
102
  WU_COMPLETION: 'wu_completion',
@@ -1091,7 +1093,6 @@ function recordTransactionState(id, wuPath, stampPath, backlogPath, statusPath)
1091
1093
  * @param {string} backlogPath - Path to backlog.md (WU-1230)
1092
1094
  * @param {string} statusPath - Path to status.md (WU-1230)
1093
1095
  */
1094
- // eslint-disable-next-line sonarjs/cognitive-complexity -- Pre-existing complexity, refactor tracked separately
1095
1096
  async function rollbackTransaction(txState, wuPath, stampPath, backlogPath, statusPath) {
1096
1097
  console.error(`\n${LOG_PREFIX.DONE} ${EMOJI.WARNING} ROLLING BACK TRANSACTION (WU-755 + WU-1230 + WU-1255 + WU-1280)...`);
1097
1098
  // WU-1280: ATOMIC ROLLBACK - Clean git state FIRST, then restore files
@@ -1272,7 +1273,6 @@ function runWUValidator(doc, id, allowTodo = false, worktreePath = null) {
1272
1273
  * @param {string|null} overrideReason - Reason for override
1273
1274
  * @returns {{valid: boolean, error: string|null, auditEntry: object|null}}
1274
1275
  */
1275
- // eslint-disable-next-line sonarjs/cognitive-complexity -- Pre-existing complexity, refactor tracked separately
1276
1276
  async function checkOwnership(id, doc, worktreePath, overrideOwner = false, overrideReason = null) {
1277
1277
  // Missing worktree means WU was not claimed properly (unless escape hatch applies)
1278
1278
  if (!worktreePath || !existsSync(worktreePath)) {
@@ -2085,7 +2085,9 @@ async function main() {
2085
2085
  if (lane)
2086
2086
  releaseLaneLock(lane, { wuId: id });
2087
2087
  }
2088
- catch { }
2088
+ catch {
2089
+ // Intentionally ignore lock release errors during cleanup
2090
+ }
2089
2091
  process.exit(EXIT_CODES.SUCCESS);
2090
2092
  }
2091
2093
  }
@@ -2096,7 +2098,9 @@ async function main() {
2096
2098
  if (lane)
2097
2099
  releaseLaneLock(lane, { wuId: id });
2098
2100
  }
2099
- catch { }
2101
+ catch {
2102
+ // Intentionally ignore lock release errors during error handling
2103
+ }
2100
2104
  // WU-1811: Check if cleanup is safe before removing worktree
2101
2105
  // If cleanupSafe is false (or undefined), preserve worktree for recovery
2102
2106
  if (err.cleanupSafe === false) {
@@ -2180,6 +2184,9 @@ async function main() {
2180
2184
  // WU-1983: Migration deployment nudge - only if supabase paths in code_paths
2181
2185
  const codePaths = docMain.code_paths || [];
2182
2186
  await printMigrationDeploymentNudge(codePaths, mainCheckoutPath);
2187
+ // WU-1366: Auto state cleanup after successful completion
2188
+ // Non-fatal: errors are logged but do not block completion
2189
+ await runAutoCleanupAfterDone(mainCheckoutPath);
2183
2190
  }
2184
2191
  /**
2185
2192
  * WU-1983: Print migration deployment nudge when WU includes supabase changes.
package/dist/wu-edit.js CHANGED
@@ -59,7 +59,6 @@ import { normalizeWUSchema } from '@lumenflow/core/dist/wu-schema-normalization.
59
59
  import { lintWUSpec, formatLintErrors } from '@lumenflow/core/dist/wu-lint.js';
60
60
  // WU-1329: Import path existence validators for strict validation
61
61
  import { validateCodePathsExistence, validateTestPathsExistence, } from '@lumenflow/core/dist/wu-preflight-validators.js';
62
- /* eslint-disable security/detect-object-injection */
63
62
  const PREFIX = LOG_PREFIX.EDIT;
64
63
  /**
65
64
  * WU-1039: Validate which edits are allowed on done WUs
@@ -271,13 +270,11 @@ const EDIT_OPTIONS = {
271
270
  * @param {string} newInitId - New initiative ID
272
271
  * @returns {Array<string>} Array of relative file paths that were modified
273
272
  */
274
- // eslint-disable-next-line sonarjs/cognitive-complexity -- Pre-existing complexity, refactor tracked separately
275
273
  function updateInitiativeWusArrays(worktreePath, wuId, oldInitId, newInitId) {
276
274
  const modifiedFiles = [];
277
275
  // Remove from old initiative if it exists and is different from new
278
276
  if (oldInitId && oldInitId !== newInitId) {
279
277
  const oldInitPath = join(worktreePath, INIT_PATHS.INITIATIVE(oldInitId));
280
- // eslint-disable-next-line security/detect-non-literal-fs-filename
281
278
  if (existsSync(oldInitPath)) {
282
279
  try {
283
280
  const oldInit = readInitiative(oldInitPath, oldInitId);
@@ -296,7 +293,6 @@ function updateInitiativeWusArrays(worktreePath, wuId, oldInitId, newInitId) {
296
293
  }
297
294
  // Add to new initiative
298
295
  const newInitPath = join(worktreePath, INIT_PATHS.INITIATIVE(newInitId));
299
- // eslint-disable-next-line security/detect-non-literal-fs-filename
300
296
  if (existsSync(newInitPath)) {
301
297
  try {
302
298
  const newInit = readInitiative(newInitPath, newInitId);
@@ -333,7 +329,6 @@ function validateInitiativeFormat(initId) {
333
329
  */
334
330
  function validateInitiativeExists(initId) {
335
331
  const initPath = INIT_PATHS.INITIATIVE(initId);
336
- // eslint-disable-next-line security/detect-non-literal-fs-filename
337
332
  if (!existsSync(initPath)) {
338
333
  die(`Initiative not found: ${initId}\n\nFile does not exist: ${initPath}`);
339
334
  }
@@ -464,11 +459,9 @@ function normalizeWUDates(wu) {
464
459
  */
465
460
  function validateWUEditable(id) {
466
461
  const wuPath = WU_PATHS.WU(id);
467
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool validates WU files
468
462
  if (!existsSync(wuPath)) {
469
463
  die(`WU ${id} not found at ${wuPath}\n\nEnsure the WU exists and you're in the repo root.`);
470
464
  }
471
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool validates WU files
472
465
  const content = readFileSync(wuPath, { encoding: FILE_SYSTEM.ENCODING });
473
466
  const wu = parseYAML(content);
474
467
  // WU-1929: Done WUs allow initiative/phase edits only (metadata reassignment)
@@ -505,7 +498,6 @@ function validateWUEditable(id) {
505
498
  * @param {string} id - WU ID (for error messages)
506
499
  */
507
500
  function validateWorktreeExists(worktreePath, id) {
508
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool validates worktree paths
509
501
  if (!existsSync(worktreePath)) {
510
502
  die(`Cannot edit WU ${id}: worktree path missing from disk.\n\n` +
511
503
  `Expected worktree at: ${worktreePath}\n\n` +
@@ -580,7 +572,6 @@ async function applyEditsInWorktree({ worktreePath, id, updatedWU }) {
580
572
  normalizeWUDates(updatedWU);
581
573
  // Emergency fix Session 2: Use centralized stringifyYAML helper
582
574
  const yamlContent = stringifyYAML(updatedWU);
583
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool writes WU files
584
575
  writeFileSync(wuPath, yamlContent, { encoding: FILE_SYSTEM.ENCODING });
585
576
  console.log(`${PREFIX} ✅ Updated ${id}.yaml in worktree`);
586
577
  // Format the file
@@ -660,11 +651,9 @@ export function mergeStringField(existing, newValue, shouldReplace) {
660
651
  */
661
652
  function loadSpecFile(specPath, originalWU) {
662
653
  const resolvedPath = resolve(specPath);
663
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool validates spec files
664
654
  if (!existsSync(resolvedPath)) {
665
655
  die(`Spec file not found: ${resolvedPath}`);
666
656
  }
667
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool validates spec files
668
657
  const specContent = readFileSync(resolvedPath, {
669
658
  encoding: FILE_SYSTEM.ENCODING,
670
659
  });
@@ -1040,7 +1029,6 @@ async function main() {
1040
1029
  normalizeWUDates(normalizedWU);
1041
1030
  // Emergency fix Session 2: Use centralized stringifyYAML helper
1042
1031
  const yamlContent = stringifyYAML(normalizedWU);
1043
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool writes WU files
1044
1032
  writeFileSync(wuPath, yamlContent, { encoding: FILE_SYSTEM.ENCODING });
1045
1033
  console.log(`${PREFIX} ✅ Updated ${id}.yaml in micro-worktree`);
1046
1034
  // WU-1929: Handle bidirectional initiative updates