@lumenflow/cli 2.7.0 → 2.9.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 (84) hide show
  1. package/README.md +121 -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__/commands.test.js +75 -0
  5. package/dist/__tests__/doctor.test.js +510 -0
  6. package/dist/__tests__/gates-config.test.js +0 -1
  7. package/dist/__tests__/hooks/enforcement.test.js +279 -0
  8. package/dist/__tests__/init-greenfield.test.js +247 -0
  9. package/dist/__tests__/init-quick-ref.test.js +0 -1
  10. package/dist/__tests__/init-template-portability.test.js +0 -1
  11. package/dist/__tests__/init.test.js +249 -0
  12. package/dist/__tests__/initiative-e2e.test.js +442 -0
  13. package/dist/__tests__/initiative-plan-replacement.test.js +0 -1
  14. package/dist/__tests__/memory-integration.test.js +333 -0
  15. package/dist/__tests__/release.test.js +1 -1
  16. package/dist/__tests__/safe-git.test.js +0 -1
  17. package/dist/__tests__/state-doctor.test.js +54 -0
  18. package/dist/__tests__/sync-templates.test.js +255 -0
  19. package/dist/__tests__/wu-create-required-fields.test.js +121 -0
  20. package/dist/__tests__/wu-done-auto-cleanup.test.js +135 -0
  21. package/dist/__tests__/wu-lifecycle-integration.test.js +388 -0
  22. package/dist/backlog-prune.js +0 -1
  23. package/dist/cli-entry-point.js +0 -1
  24. package/dist/commands/integrate.js +229 -0
  25. package/dist/commands.js +171 -0
  26. package/dist/docs-sync.js +46 -0
  27. package/dist/doctor.js +479 -10
  28. package/dist/gates.js +0 -7
  29. package/dist/hooks/enforcement-checks.js +209 -0
  30. package/dist/hooks/enforcement-generator.js +365 -0
  31. package/dist/hooks/enforcement-sync.js +243 -0
  32. package/dist/hooks/index.js +7 -0
  33. package/dist/init.js +502 -17
  34. package/dist/initiative-add-wu.js +0 -2
  35. package/dist/initiative-create.js +0 -3
  36. package/dist/initiative-edit.js +0 -5
  37. package/dist/initiative-plan.js +0 -1
  38. package/dist/initiative-remove-wu.js +0 -2
  39. package/dist/lane-health.js +0 -2
  40. package/dist/lane-suggest.js +0 -1
  41. package/dist/mem-checkpoint.js +0 -2
  42. package/dist/mem-cleanup.js +0 -2
  43. package/dist/mem-context.js +0 -3
  44. package/dist/mem-create.js +0 -2
  45. package/dist/mem-delete.js +0 -3
  46. package/dist/mem-inbox.js +0 -2
  47. package/dist/mem-index.js +0 -1
  48. package/dist/mem-init.js +0 -2
  49. package/dist/mem-profile.js +0 -1
  50. package/dist/mem-promote.js +0 -1
  51. package/dist/mem-ready.js +0 -2
  52. package/dist/mem-signal.js +0 -2
  53. package/dist/mem-start.js +0 -2
  54. package/dist/mem-summarize.js +0 -2
  55. package/dist/metrics-cli.js +1 -1
  56. package/dist/metrics-snapshot.js +1 -1
  57. package/dist/onboarding-smoke-test.js +0 -5
  58. package/dist/orchestrate-init-status.js +0 -1
  59. package/dist/orchestrate-initiative.js +0 -1
  60. package/dist/orchestrate-monitor.js +0 -1
  61. package/dist/plan-create.js +0 -2
  62. package/dist/plan-edit.js +0 -2
  63. package/dist/plan-link.js +0 -2
  64. package/dist/plan-promote.js +0 -2
  65. package/dist/signal-cleanup.js +0 -4
  66. package/dist/state-bootstrap.js +0 -1
  67. package/dist/state-cleanup.js +0 -4
  68. package/dist/state-doctor-fix.js +5 -8
  69. package/dist/state-doctor.js +0 -11
  70. package/dist/sync-templates.js +188 -34
  71. package/dist/wu-block.js +100 -48
  72. package/dist/wu-claim.js +1 -22
  73. package/dist/wu-cleanup.js +0 -1
  74. package/dist/wu-create.js +0 -2
  75. package/dist/wu-done-auto-cleanup.js +139 -0
  76. package/dist/wu-done.js +11 -4
  77. package/dist/wu-edit.js +0 -12
  78. package/dist/wu-preflight.js +0 -1
  79. package/dist/wu-prep.js +0 -1
  80. package/dist/wu-proto.js +0 -1
  81. package/dist/wu-spawn.js +0 -3
  82. package/dist/wu-unblock.js +0 -2
  83. package/dist/wu-validate.js +0 -1
  84. package/package.json +9 -7
@@ -2,24 +2,29 @@
2
2
  * @file sync-templates.ts
3
3
  * Sync internal docs to CLI templates for release-cycle maintenance (WU-1123)
4
4
  *
5
+ * WU-1368: Fixed two bugs:
6
+ * 1. --check-drift flag now is truly read-only (compares without writing)
7
+ * 2. sync:templates uses micro-worktree isolation for safe atomic commits
8
+ *
5
9
  * This script syncs source docs from the hellmai/os repo to the templates
6
10
  * directory, applying template variable substitutions:
7
11
  * - Onboarding docs -> templates/core/ai/onboarding/
8
12
  * - Claude skills -> templates/vendors/claude/.claude/skills/
9
13
  * - Core docs (LUMENFLOW.md, constraints.md) -> templates/core/
10
14
  */
11
- /* eslint-disable no-console -- CLI tool requires console output */
12
- /* eslint-disable security/detect-non-literal-fs-filename -- CLI tool syncs templates from known paths */
13
- /* eslint-disable security/detect-non-literal-regexp -- Dynamic date pattern for template substitution */
14
15
  import * as fs from 'node:fs';
15
16
  import * as path from 'node:path';
16
- import { createWUParser } from '@lumenflow/core';
17
+ import { createWUParser, withMicroWorktree } from '@lumenflow/core';
17
18
  // Directory name constants to avoid duplicate strings
18
19
  const LUMENFLOW_DIR = '.lumenflow';
19
20
  const CLAUDE_DIR = '.claude';
20
21
  const SKILLS_DIR = 'skills';
21
22
  // Template variable patterns
22
23
  const DATE_PATTERN = /\d{4}-\d{2}-\d{2}/g;
24
+ // Log prefix for console output
25
+ const LOG_PREFIX = '[sync-templates]';
26
+ // Micro-worktree operation name
27
+ const OPERATION_NAME = 'sync-templates';
23
28
  /**
24
29
  * CLI option definitions for sync-templates command
25
30
  */
@@ -212,7 +217,6 @@ function checkFileDrift(sourcePath, templatePath, projectRoot) {
212
217
  * to detect if templates have drifted out of sync. Used by CI to warn
213
218
  * when templates need to be re-synced.
214
219
  */
215
- // eslint-disable-next-line sonarjs/cognitive-complexity -- Multi-category drift check requires nested iteration
216
220
  export async function checkTemplateDrift(projectRoot) {
217
221
  const driftingFiles = [];
218
222
  const checkedFiles = [];
@@ -277,40 +281,145 @@ export async function checkTemplateDrift(projectRoot) {
277
281
  };
278
282
  }
279
283
  /**
280
- * CLI entry point
284
+ * Sync a single file to templates within a worktree path
285
+ *
286
+ * WU-1368: Internal helper for micro-worktree sync operations.
287
+ * Writes to worktreePath instead of projectRoot for isolation.
281
288
  */
282
- // eslint-disable-next-line sonarjs/cognitive-complexity -- CLI main() handles multiple modes and output formatting
283
- export async function main() {
284
- const opts = parseSyncTemplatesOptions();
285
- const projectRoot = process.cwd();
286
- // Check-drift mode: verify templates match source without syncing
287
- if (opts.checkDrift) {
288
- console.log('[sync-templates] Checking for template drift...');
289
- const drift = await checkTemplateDrift(projectRoot);
290
- if (opts.verbose) {
291
- console.log(` Checked ${drift.checkedFiles.length} files`);
289
+ function syncFileToWorktree(sourcePath, targetPath, projectRoot, result) {
290
+ try {
291
+ if (!fs.existsSync(sourcePath)) {
292
+ result.errors.push(`Source not found: ${sourcePath}`);
293
+ return;
292
294
  }
293
- if (drift.hasDrift) {
294
- console.log('\n[sync-templates] WARNING: Template drift detected!');
295
- console.log(' The following templates are out of sync with their source:');
296
- for (const file of drift.driftingFiles) {
297
- console.log(` - ${file}`);
298
- }
299
- console.log('\n Run `pnpm sync:templates` to update templates.');
300
- process.exitCode = 1;
295
+ const content = fs.readFileSync(sourcePath, 'utf-8');
296
+ const templateContent = convertToTemplate(content, projectRoot);
297
+ ensureDir(path.dirname(targetPath));
298
+ fs.writeFileSync(targetPath, templateContent);
299
+ // Store relative path from project root (not worktree path)
300
+ const relPath = targetPath.includes('templates/')
301
+ ? targetPath.substring(targetPath.indexOf('packages/'))
302
+ : path.basename(targetPath);
303
+ result.synced.push(relPath);
304
+ }
305
+ catch (error) {
306
+ result.errors.push(`Error syncing ${sourcePath}: ${error.message}`);
307
+ }
308
+ }
309
+ /**
310
+ * Sync templates using micro-worktree isolation (WU-1368)
311
+ *
312
+ * This function uses the micro-worktree pattern to atomically sync templates:
313
+ * 1. Create temp branch in micro-worktree
314
+ * 2. Sync all templates to micro-worktree
315
+ * 3. Commit and push atomically
316
+ * 4. Cleanup
317
+ *
318
+ * Benefits:
319
+ * - Never modifies main checkout directly
320
+ * - Atomic commit with all template changes
321
+ * - Race-safe with other operations
322
+ *
323
+ * @param {string} projectRoot - Project root directory (source for templates)
324
+ * @returns {Promise<SyncSummary>} Summary of synced files
325
+ */
326
+ export async function syncTemplatesWithWorktree(projectRoot) {
327
+ // Generate unique operation ID using timestamp
328
+ const operationId = `templates-${Date.now()}`;
329
+ console.log(`${LOG_PREFIX} Using micro-worktree isolation for atomic sync...`);
330
+ // Set env var for pre-push hook
331
+ const previousWuTool = process.env.LUMENFLOW_WU_TOOL;
332
+ process.env.LUMENFLOW_WU_TOOL = OPERATION_NAME;
333
+ try {
334
+ let syncResult = {
335
+ onboarding: { synced: [], errors: [] },
336
+ skills: { synced: [], errors: [] },
337
+ core: { synced: [], errors: [] },
338
+ };
339
+ await withMicroWorktree({
340
+ operation: OPERATION_NAME,
341
+ id: operationId,
342
+ logPrefix: LOG_PREFIX,
343
+ execute: async ({ worktreePath }) => {
344
+ const templatesDir = path.join(worktreePath, 'packages', '@lumenflow', 'cli', 'templates');
345
+ // Sync core docs
346
+ const coreResult = { synced: [], errors: [] };
347
+ const lumenflowSource = path.join(projectRoot, 'LUMENFLOW.md');
348
+ const lumenflowTarget = path.join(templatesDir, 'core', 'LUMENFLOW.md.template');
349
+ syncFileToWorktree(lumenflowSource, lumenflowTarget, projectRoot, coreResult);
350
+ const constraintsSource = path.join(projectRoot, LUMENFLOW_DIR, 'constraints.md');
351
+ const constraintsTarget = path.join(templatesDir, 'core', LUMENFLOW_DIR, 'constraints.md.template');
352
+ syncFileToWorktree(constraintsSource, constraintsTarget, projectRoot, coreResult);
353
+ // Sync onboarding docs
354
+ const onboardingResult = { synced: [], errors: [] };
355
+ const onboardingSourceDir = path.join(projectRoot, 'docs', '04-operations', '_frameworks', 'lumenflow', 'agent', 'onboarding');
356
+ const onboardingTargetDir = path.join(templatesDir, 'core', 'ai', 'onboarding');
357
+ if (fs.existsSync(onboardingSourceDir)) {
358
+ const files = fs.readdirSync(onboardingSourceDir).filter((f) => f.endsWith('.md'));
359
+ for (const file of files) {
360
+ const sourcePath = path.join(onboardingSourceDir, file);
361
+ const targetPath = path.join(onboardingTargetDir, `${file}.template`);
362
+ syncFileToWorktree(sourcePath, targetPath, projectRoot, onboardingResult);
363
+ }
364
+ }
365
+ else {
366
+ onboardingResult.errors.push(`Onboarding source directory not found: ${onboardingSourceDir}`);
367
+ }
368
+ // Sync skills
369
+ const skillsResult = { synced: [], errors: [] };
370
+ const skillsSourceDir = path.join(projectRoot, CLAUDE_DIR, SKILLS_DIR);
371
+ const skillsTargetDir = path.join(templatesDir, 'vendors', 'claude', CLAUDE_DIR, SKILLS_DIR);
372
+ if (fs.existsSync(skillsSourceDir)) {
373
+ const skillDirs = fs
374
+ .readdirSync(skillsSourceDir, { withFileTypes: true })
375
+ .filter((d) => d.isDirectory())
376
+ .map((d) => d.name);
377
+ for (const skillName of skillDirs) {
378
+ const skillFile = path.join(skillsSourceDir, skillName, 'SKILL.md');
379
+ if (fs.existsSync(skillFile)) {
380
+ const targetPath = path.join(skillsTargetDir, skillName, 'SKILL.md.template');
381
+ syncFileToWorktree(skillFile, targetPath, projectRoot, skillsResult);
382
+ }
383
+ }
384
+ }
385
+ else {
386
+ skillsResult.errors.push(`Skills source directory not found: ${skillsSourceDir}`);
387
+ }
388
+ syncResult = {
389
+ onboarding: onboardingResult,
390
+ skills: skillsResult,
391
+ core: coreResult,
392
+ };
393
+ // Collect all synced files for commit
394
+ const allSyncedFiles = [
395
+ ...coreResult.synced,
396
+ ...onboardingResult.synced,
397
+ ...skillsResult.synced,
398
+ ];
399
+ const totalSynced = allSyncedFiles.length;
400
+ const commitMessage = `chore(sync:templates): sync ${totalSynced} template files`;
401
+ return {
402
+ commitMessage,
403
+ files: allSyncedFiles,
404
+ };
405
+ },
406
+ });
407
+ return syncResult;
408
+ }
409
+ finally {
410
+ // Restore env var
411
+ if (previousWuTool === undefined) {
412
+ delete process.env.LUMENFLOW_WU_TOOL;
301
413
  }
302
414
  else {
303
- console.log('[sync-templates] All templates are in sync.');
415
+ process.env.LUMENFLOW_WU_TOOL = previousWuTool;
304
416
  }
305
- return;
306
- }
307
- // Sync mode: update templates from source
308
- console.log('[sync-templates] Syncing internal docs to CLI templates...');
309
- if (opts.dryRun) {
310
- console.log(' (dry-run mode - no files will be written)');
311
417
  }
312
- const result = await syncTemplates(projectRoot, opts.dryRun);
313
- // Print results
418
+ }
419
+ /**
420
+ * Print sync results summary
421
+ */
422
+ function printSyncResults(result) {
314
423
  const sections = [
315
424
  { name: 'Onboarding docs', data: result.onboarding },
316
425
  { name: 'Claude skills', data: result.skills },
@@ -331,7 +440,52 @@ export async function main() {
331
440
  }
332
441
  }
333
442
  }
334
- console.log(`\n[sync-templates] Done! Synced ${totalSynced} files.`);
443
+ return { totalSynced, totalErrors };
444
+ }
445
+ /**
446
+ * CLI entry point
447
+ */
448
+ export async function main() {
449
+ const opts = parseSyncTemplatesOptions();
450
+ const projectRoot = process.cwd();
451
+ // Check-drift mode: verify templates match source without syncing (read-only)
452
+ if (opts.checkDrift) {
453
+ console.log(`${LOG_PREFIX} Checking for template drift...`);
454
+ const drift = await checkTemplateDrift(projectRoot);
455
+ if (opts.verbose) {
456
+ console.log(` Checked ${drift.checkedFiles.length} files`);
457
+ }
458
+ if (drift.hasDrift) {
459
+ console.log(`\n${LOG_PREFIX} WARNING: Template drift detected!`);
460
+ console.log(' The following templates are out of sync with their source:');
461
+ for (const file of drift.driftingFiles) {
462
+ console.log(` - ${file}`);
463
+ }
464
+ console.log('\n Run `pnpm sync:templates` to update templates.');
465
+ process.exitCode = 1;
466
+ }
467
+ else {
468
+ console.log(`${LOG_PREFIX} All templates are in sync.`);
469
+ }
470
+ return;
471
+ }
472
+ // Dry-run mode: show what would be synced without writing
473
+ if (opts.dryRun) {
474
+ console.log(`${LOG_PREFIX} Dry-run mode - showing what would be synced...`);
475
+ const result = await syncTemplates(projectRoot, true);
476
+ const { totalSynced, totalErrors } = printSyncResults(result);
477
+ console.log(`\n${LOG_PREFIX} Dry run complete. Would sync ${totalSynced} files.`);
478
+ if (totalErrors > 0) {
479
+ console.log(` ${totalErrors} error(s) would occur.`);
480
+ process.exitCode = 1;
481
+ }
482
+ return;
483
+ }
484
+ // Sync mode: update templates using micro-worktree isolation (WU-1368)
485
+ console.log(`${LOG_PREFIX} Syncing internal docs to CLI templates...`);
486
+ const result = await syncTemplatesWithWorktree(projectRoot);
487
+ const { totalSynced, totalErrors } = printSyncResults(result);
488
+ console.log(`\n${LOG_PREFIX} Done! Synced ${totalSynced} files.`);
335
489
  if (totalErrors > 0) {
336
490
  console.log(` ${totalErrors} error(s) occurred.`);
337
491
  process.exitCode = 1;
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',