@lumenflow/core 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.
@@ -1,4 +1,3 @@
1
- /* eslint-disable security/detect-non-literal-fs-filename, security/detect-object-injection */
2
1
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
3
2
  import matter from 'gray-matter';
4
3
  import { createError, ErrorCodes } from './error-handler.js';
@@ -133,7 +133,7 @@ sections:
133
133
  insertion: after_heading_blank_line
134
134
  ---
135
135
 
136
- > Agent: Read **docs/04-operations/_frameworks/lumenflow/agent/onboarding/starting-prompt.md** first, then follow **docs/04-operations/\_frameworks/lumenflow/lumenflow-complete.md** for execution.
136
+ > Agent: Read **docs/04-operations/_frameworks/lumenflow/agent/onboarding/starting-prompt.md** first, then follow **docs/04-operations/_frameworks/lumenflow/lumenflow-complete.md** for execution.
137
137
 
138
138
  # Backlog (single source of truth)
139
139
 
@@ -20,7 +20,6 @@
20
20
  *
21
21
  * @module cleanup-lock
22
22
  */
23
- /* eslint-disable security/detect-non-literal-fs-filename -- Lock file paths are computed from trusted sources */
24
23
  import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync, openSync, closeSync, } from 'node:fs';
25
24
  import path from 'node:path';
26
25
  import crypto from 'node:crypto';
@@ -21,7 +21,6 @@
21
21
  *
22
22
  * Part of INIT-023: Workflow Integrity initiative.
23
23
  */
24
- /* eslint-disable security/detect-non-literal-fs-filename, security/detect-object-injection */
25
24
  import path from 'node:path';
26
25
  import { existsSync, readFileSync } from 'node:fs';
27
26
  import { execSync } from 'node:child_process';
@@ -12,7 +12,6 @@
12
12
  * Legacy format (v1): timestamp | command | branch | worktree
13
13
  * - Backward compatible: old entries are parsed with user='unknown' and outcome='unknown'
14
14
  */
15
- /* eslint-disable security/detect-non-literal-fs-filename */
16
15
  import fs from 'node:fs';
17
16
  import path from 'node:path';
18
17
  import { fileURLToPath } from 'node:url';
@@ -221,7 +221,7 @@ export class ComplianceParser {
221
221
  }
222
222
  // Parse blockers
223
223
  if (inBlockers && trimmedLine.startsWith('-') && !trimmedLine.includes('None')) {
224
- const blockerText = trimmedLine.replace(/^-\s*/, '').replace(/^[🔴🟡]\s*/, '');
224
+ const blockerText = trimmedLine.replace(/^-\s*/, '').replace(/^[🔴🟡]\s*/u, '');
225
225
  if (blockerText.trim()) {
226
226
  currentGap.blockers.push(blockerText.trim());
227
227
  // Also extract any GAP references
@@ -182,7 +182,6 @@ export function loadGatesConfig(projectRoot) {
182
182
  // Validate the config
183
183
  const result = GatesExecutionConfigSchema.safeParse(executionConfig);
184
184
  if (!result.success) {
185
- // eslint-disable-next-line no-console -- Intentional warning for invalid config
186
185
  console.warn('Warning: Invalid gates.execution config:', result.error.message);
187
186
  return null;
188
187
  }
@@ -195,7 +194,6 @@ export function loadGatesConfig(projectRoot) {
195
194
  return merged;
196
195
  }
197
196
  catch (error) {
198
- // eslint-disable-next-line no-console -- Intentional warning for parse failure
199
197
  console.warn(`Warning: Failed to parse ${CONFIG_FILE_NAME}:`, error instanceof Error ? error.message : String(error));
200
198
  return null;
201
199
  }
@@ -291,14 +289,12 @@ export function loadLaneHealthConfig(projectRoot) {
291
289
  // Validate the config value
292
290
  const result = LaneHealthModeSchema.safeParse(laneHealthConfig);
293
291
  if (!result.success) {
294
- // eslint-disable-next-line no-console -- Intentional warning for invalid config
295
292
  console.warn(`Warning: Invalid gates.lane_health value '${laneHealthConfig}', expected 'warn', 'error', or 'off'. Using default 'warn'.`);
296
293
  return DEFAULT_LANE_HEALTH_MODE;
297
294
  }
298
295
  return result.data;
299
296
  }
300
297
  catch (error) {
301
- // eslint-disable-next-line no-console -- Intentional warning for parse failure
302
298
  console.warn(`Warning: Failed to parse ${CONFIG_FILE_NAME} for lane_health config:`, error instanceof Error ? error.message : String(error));
303
299
  return DEFAULT_LANE_HEALTH_MODE;
304
300
  }
@@ -485,7 +485,6 @@ export class GitAdapter {
485
485
  catch (err) {
486
486
  // If git fails, we still want to clean up the directory
487
487
  // Re-throw after cleanup attempt to report the original error
488
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool with validated worktree path
489
488
  if (existsSync(worktreePath)) {
490
489
  rmSync(worktreePath, { recursive: true, force: true });
491
490
  }
@@ -493,7 +492,6 @@ export class GitAdapter {
493
492
  }
494
493
  // Layer 1 defense (WU-1476): Explicit cleanup if directory still exists
495
494
  // This handles edge cases where git worktree remove succeeds but leaves the directory
496
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool with validated worktree path
497
495
  if (existsSync(worktreePath)) {
498
496
  try {
499
497
  rmSync(worktreePath, { recursive: true, force: true });
@@ -90,7 +90,6 @@ function safeGitExec(args, cwd) {
90
90
  // 1. 'git' is a fixed command from PATH (trusted)
91
91
  // 2. args are internally constructed, not from user input
92
92
  // 3. cwd is validated by the caller (projectRoot from CLI)
93
- // eslint-disable-next-line sonarjs/os-command -- safe: git is trusted, args are static
94
93
  const result = execSync(cmd, {
95
94
  cwd,
96
95
  encoding: 'utf-8',
@@ -124,7 +123,6 @@ export function extractGitContext(projectRoot, options = {}) {
124
123
  try {
125
124
  // Check if this is a git repo using execSync directly
126
125
  // SECURITY: This is a static command with no user input
127
- // eslint-disable-next-line sonarjs/no-os-command-from-path -- safe: static command
128
126
  execSync('git rev-parse --is-inside-work-tree', {
129
127
  cwd: projectRoot,
130
128
  encoding: 'utf-8',
@@ -441,7 +439,6 @@ export function getChurnMetrics(projectRoot, options = {}) {
441
439
  try {
442
440
  /// SECURITY: all args are constructed internally (no user input)
443
441
  const cmd = ['git', ...args].join(' ');
444
- // eslint-disable-next-line sonarjs/os-command -- safe: git is trusted, args are static
445
442
  output = execSync(cmd, {
446
443
  cwd: projectRoot,
447
444
  encoding: 'utf-8',
@@ -200,7 +200,6 @@ function validateParentOnlyFormat(trimmed, configPath, strict) {
200
200
  throw createError(ErrorCodes.INVALID_LANE, message, { lane: trimmed, validSubLanes });
201
201
  }
202
202
  // Non-strict mode: warn only for existing WU validation
203
- // eslint-disable-next-line no-console -- Intentional operational logging
204
203
  console.warn(`${PREFIX} ⚠️ ${message}`);
205
204
  }
206
205
  return { valid: true, parent: trimmed, error: null };
@@ -514,7 +513,6 @@ function checkWuLaneMatch(activeWuid, wuid, projectRoot, targetLane) {
514
513
  }
515
514
  const wuPath = path.join(projectRoot, WU_PATHS.WU(activeWuid));
516
515
  if (!existsSync(wuPath)) {
517
- // eslint-disable-next-line no-console -- Intentional operational logging
518
516
  console.warn(`${PREFIX} Warning: ${activeWuid} referenced in status.md but ${wuPath} not found`);
519
517
  return null;
520
518
  }
@@ -522,7 +520,6 @@ function checkWuLaneMatch(activeWuid, wuid, projectRoot, targetLane) {
522
520
  const wuContent = readFileSync(wuPath, { encoding: 'utf-8' });
523
521
  const wuDoc = parseYAML(wuContent);
524
522
  if (!wuDoc || !wuDoc.lane) {
525
- // eslint-disable-next-line no-console -- Intentional operational logging
526
523
  console.warn(`${PREFIX} Warning: ${activeWuid} has no lane field`);
527
524
  return null;
528
525
  }
@@ -534,7 +531,6 @@ function checkWuLaneMatch(activeWuid, wuid, projectRoot, targetLane) {
534
531
  }
535
532
  catch (e) {
536
533
  const errMessage = e instanceof Error ? e.message : String(e);
537
- // eslint-disable-next-line no-console -- Intentional operational logging
538
534
  console.warn(`${PREFIX} Warning: Failed to parse ${activeWuid} YAML: ${errMessage}`);
539
535
  }
540
536
  return null;
@@ -692,12 +688,10 @@ export function checkWipJustification(lane, options = {}) {
692
688
  const projectRoot = findProjectRoot();
693
689
  resolvedConfigPath = path.join(projectRoot, CONFIG_FILES.LUMENFLOW_CONFIG);
694
690
  }
695
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- Config path is validated
696
691
  if (!existsSync(resolvedConfigPath)) {
697
692
  return NO_JUSTIFICATION_REQUIRED;
698
693
  }
699
694
  try {
700
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- Config path is validated
701
695
  const configContent = readFileSync(resolvedConfigPath, { encoding: 'utf-8' });
702
696
  const config = parseYAML(configContent);
703
697
  const allLanes = extractAllLanesFromConfig(config);
package/dist/lane-lock.js CHANGED
@@ -180,7 +180,6 @@ export function acquireLaneLock(lane, wuId, options = {}) {
180
180
  // WU-1325: Check lock policy before acquiring
181
181
  const lockPolicy = getLockPolicyForLane(lane);
182
182
  if (lockPolicy === 'none') {
183
- // eslint-disable-next-line no-console -- CLI tool status message
184
183
  console.log(`${LOG_PREFIX} Skipping lock acquisition for "${lane}" (lock_policy=none)`);
185
184
  return {
186
185
  acquired: true,
@@ -347,12 +347,40 @@ export declare const ClientSkillsSchema: z.ZodObject<{
347
347
  recommended: z.ZodDefault<z.ZodArray<z.ZodString>>;
348
348
  byLane: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
349
349
  }, z.core.$strip>;
350
+ /**
351
+ * WU-1367: Client enforcement configuration
352
+ *
353
+ * Configures workflow compliance enforcement via Claude Code hooks.
354
+ * When enabled, hooks block non-compliant operations instead of relying
355
+ * on agents to remember workflow rules.
356
+ *
357
+ * @example
358
+ * ```yaml
359
+ * agents:
360
+ * clients:
361
+ * claude-code:
362
+ * enforcement:
363
+ * hooks: true
364
+ * block_outside_worktree: true
365
+ * require_wu_for_edits: true
366
+ * warn_on_stop_without_wu_done: true
367
+ * ```
368
+ */
369
+ export declare const ClientEnforcementSchema: z.ZodObject<{
370
+ hooks: z.ZodDefault<z.ZodBoolean>;
371
+ block_outside_worktree: z.ZodDefault<z.ZodBoolean>;
372
+ require_wu_for_edits: z.ZodDefault<z.ZodBoolean>;
373
+ warn_on_stop_without_wu_done: z.ZodDefault<z.ZodBoolean>;
374
+ }, z.core.$strip>;
375
+ /** WU-1367: TypeScript type for client enforcement config */
376
+ export type ClientEnforcement = z.infer<typeof ClientEnforcementSchema>;
350
377
  /**
351
378
  * Client configuration (per-client settings)
352
379
  */
353
380
  export declare const ClientConfigSchema: z.ZodObject<{
354
381
  preamble: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodBoolean]>>;
355
382
  skillsDir: z.ZodOptional<z.ZodString>;
383
+ agentsDir: z.ZodOptional<z.ZodString>;
356
384
  blocks: z.ZodDefault<z.ZodArray<z.ZodObject<{
357
385
  title: z.ZodString;
358
386
  content: z.ZodString;
@@ -362,6 +390,12 @@ export declare const ClientConfigSchema: z.ZodObject<{
362
390
  recommended: z.ZodDefault<z.ZodArray<z.ZodString>>;
363
391
  byLane: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
364
392
  }, z.core.$strip>>;
393
+ enforcement: z.ZodOptional<z.ZodObject<{
394
+ hooks: z.ZodDefault<z.ZodBoolean>;
395
+ block_outside_worktree: z.ZodDefault<z.ZodBoolean>;
396
+ require_wu_for_edits: z.ZodDefault<z.ZodBoolean>;
397
+ warn_on_stop_without_wu_done: z.ZodDefault<z.ZodBoolean>;
398
+ }, z.core.$strip>>;
365
399
  }, z.core.$strip>;
366
400
  /**
367
401
  * Agents configuration
@@ -371,6 +405,7 @@ export declare const AgentsConfigSchema: z.ZodObject<{
371
405
  clients: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
372
406
  preamble: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodBoolean]>>;
373
407
  skillsDir: z.ZodOptional<z.ZodString>;
408
+ agentsDir: z.ZodOptional<z.ZodString>;
374
409
  blocks: z.ZodDefault<z.ZodArray<z.ZodObject<{
375
410
  title: z.ZodString;
376
411
  content: z.ZodString;
@@ -380,6 +415,12 @@ export declare const AgentsConfigSchema: z.ZodObject<{
380
415
  recommended: z.ZodDefault<z.ZodArray<z.ZodString>>;
381
416
  byLane: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
382
417
  }, z.core.$strip>>;
418
+ enforcement: z.ZodOptional<z.ZodObject<{
419
+ hooks: z.ZodDefault<z.ZodBoolean>;
420
+ block_outside_worktree: z.ZodDefault<z.ZodBoolean>;
421
+ require_wu_for_edits: z.ZodDefault<z.ZodBoolean>;
422
+ warn_on_stop_without_wu_done: z.ZodDefault<z.ZodBoolean>;
423
+ }, z.core.$strip>>;
383
424
  }, z.core.$strip>>>;
384
425
  methodology: z.ZodDefault<z.ZodObject<{
385
426
  enabled: z.ZodDefault<z.ZodBoolean>;
@@ -433,6 +474,41 @@ export declare const TelemetryConfigSchema: z.ZodObject<{
433
474
  enabled: z.ZodDefault<z.ZodBoolean>;
434
475
  }, z.core.$strip>>;
435
476
  }, z.core.$strip>;
477
+ /**
478
+ * WU-1366: Cleanup trigger options
479
+ *
480
+ * Controls when automatic state cleanup runs:
481
+ * - 'on_done': Run after wu:done success (default)
482
+ * - 'on_init': Run during lumenflow init
483
+ * - 'manual': Only run via pnpm state:cleanup
484
+ */
485
+ export declare const CleanupTriggerSchema: z.ZodDefault<z.ZodEnum<{
486
+ manual: "manual";
487
+ on_done: "on_done";
488
+ on_init: "on_init";
489
+ }>>;
490
+ /** WU-1366: TypeScript type for cleanup trigger */
491
+ export type CleanupTrigger = z.infer<typeof CleanupTriggerSchema>;
492
+ /**
493
+ * WU-1366: Cleanup configuration schema
494
+ *
495
+ * Controls when and how automatic state cleanup runs.
496
+ *
497
+ * @example
498
+ * ```yaml
499
+ * cleanup:
500
+ * trigger: on_done # on_done | on_init | manual
501
+ * ```
502
+ */
503
+ export declare const CleanupConfigSchema: z.ZodObject<{
504
+ trigger: z.ZodDefault<z.ZodEnum<{
505
+ manual: "manual";
506
+ on_done: "on_done";
507
+ on_init: "on_init";
508
+ }>>;
509
+ }, z.core.$strip>;
510
+ /** WU-1366: TypeScript type for cleanup config */
511
+ export type CleanupConfig = z.infer<typeof CleanupConfigSchema>;
436
512
  /**
437
513
  * WU-1345: Lane enforcement configuration schema
438
514
  *
@@ -675,6 +751,7 @@ export declare const LumenFlowConfigSchema: z.ZodObject<{
675
751
  clients: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
676
752
  preamble: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodBoolean]>>;
677
753
  skillsDir: z.ZodOptional<z.ZodString>;
754
+ agentsDir: z.ZodOptional<z.ZodString>;
678
755
  blocks: z.ZodDefault<z.ZodArray<z.ZodObject<{
679
756
  title: z.ZodString;
680
757
  content: z.ZodString;
@@ -684,6 +761,12 @@ export declare const LumenFlowConfigSchema: z.ZodObject<{
684
761
  recommended: z.ZodDefault<z.ZodArray<z.ZodString>>;
685
762
  byLane: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
686
763
  }, z.core.$strip>>;
764
+ enforcement: z.ZodOptional<z.ZodObject<{
765
+ hooks: z.ZodDefault<z.ZodBoolean>;
766
+ block_outside_worktree: z.ZodDefault<z.ZodBoolean>;
767
+ require_wu_for_edits: z.ZodDefault<z.ZodBoolean>;
768
+ warn_on_stop_without_wu_done: z.ZodDefault<z.ZodBoolean>;
769
+ }, z.core.$strip>>;
687
770
  }, z.core.$strip>>>;
688
771
  methodology: z.ZodDefault<z.ZodObject<{
689
772
  enabled: z.ZodDefault<z.ZodBoolean>;
@@ -705,6 +788,13 @@ export declare const LumenFlowConfigSchema: z.ZodObject<{
705
788
  show_next_steps: z.ZodDefault<z.ZodBoolean>;
706
789
  recovery_command: z.ZodDefault<z.ZodBoolean>;
707
790
  }, z.core.$strip>>;
791
+ cleanup: z.ZodDefault<z.ZodObject<{
792
+ trigger: z.ZodDefault<z.ZodEnum<{
793
+ manual: "manual";
794
+ on_done: "on_done";
795
+ on_init: "on_init";
796
+ }>>;
797
+ }, z.core.$strip>>;
708
798
  telemetry: z.ZodDefault<z.ZodObject<{
709
799
  methodology: z.ZodDefault<z.ZodObject<{
710
800
  enabled: z.ZodDefault<z.ZodBoolean>;
@@ -970,11 +1060,18 @@ export declare function validateConfig(data: unknown): z.ZodSafeParseResult<{
970
1060
  }[];
971
1061
  preamble?: string | boolean;
972
1062
  skillsDir?: string;
1063
+ agentsDir?: string;
973
1064
  skills?: {
974
1065
  recommended: string[];
975
1066
  instructions?: string;
976
1067
  byLane?: Record<string, string[]>;
977
1068
  };
1069
+ enforcement?: {
1070
+ hooks: boolean;
1071
+ block_outside_worktree: boolean;
1072
+ require_wu_for_edits: boolean;
1073
+ warn_on_stop_without_wu_done: boolean;
1074
+ };
978
1075
  }>;
979
1076
  methodology: {
980
1077
  enabled: boolean;
@@ -989,6 +1086,9 @@ export declare function validateConfig(data: unknown): z.ZodSafeParseResult<{
989
1086
  show_next_steps: boolean;
990
1087
  recovery_command: boolean;
991
1088
  };
1089
+ cleanup: {
1090
+ trigger: "manual" | "on_done" | "on_init";
1091
+ };
992
1092
  telemetry: {
993
1093
  methodology: {
994
1094
  enabled: boolean;
@@ -587,6 +587,51 @@ export const ClientSkillsSchema = z.object({
587
587
  */
588
588
  byLane: z.record(z.string(), z.array(z.string())).optional(),
589
589
  });
590
+ /**
591
+ * WU-1367: Client enforcement configuration
592
+ *
593
+ * Configures workflow compliance enforcement via Claude Code hooks.
594
+ * When enabled, hooks block non-compliant operations instead of relying
595
+ * on agents to remember workflow rules.
596
+ *
597
+ * @example
598
+ * ```yaml
599
+ * agents:
600
+ * clients:
601
+ * claude-code:
602
+ * enforcement:
603
+ * hooks: true
604
+ * block_outside_worktree: true
605
+ * require_wu_for_edits: true
606
+ * warn_on_stop_without_wu_done: true
607
+ * ```
608
+ */
609
+ export const ClientEnforcementSchema = z.object({
610
+ /**
611
+ * Enable enforcement hooks.
612
+ * When true, hooks are generated in .claude/hooks/
613
+ * @default false
614
+ */
615
+ hooks: z.boolean().default(false),
616
+ /**
617
+ * Block Write/Edit operations when cwd is not a worktree.
618
+ * Prevents accidental edits to main checkout.
619
+ * @default false
620
+ */
621
+ block_outside_worktree: z.boolean().default(false),
622
+ /**
623
+ * Require a claimed WU for Write/Edit operations.
624
+ * Ensures all edits are associated with tracked work.
625
+ * @default false
626
+ */
627
+ require_wu_for_edits: z.boolean().default(false),
628
+ /**
629
+ * Warn when session ends without wu:done being called.
630
+ * Reminds agents to complete their work properly.
631
+ * @default false
632
+ */
633
+ warn_on_stop_without_wu_done: z.boolean().default(false),
634
+ });
590
635
  /**
591
636
  * Client configuration (per-client settings)
592
637
  */
@@ -595,10 +640,17 @@ export const ClientConfigSchema = z.object({
595
640
  preamble: z.union([z.string(), z.boolean()]).optional(),
596
641
  /** Skills directory path */
597
642
  skillsDir: z.string().optional(),
643
+ /** Agents directory path */
644
+ agentsDir: z.string().optional(),
598
645
  /** Client-specific blocks injected into wu:spawn output */
599
646
  blocks: z.array(ClientBlockSchema).default([]),
600
647
  /** Client-specific skills guidance for wu:spawn */
601
648
  skills: ClientSkillsSchema.optional(),
649
+ /**
650
+ * WU-1367: Enforcement configuration for Claude Code hooks.
651
+ * When enabled, generates hooks that enforce workflow compliance.
652
+ */
653
+ enforcement: ClientEnforcementSchema.optional(),
602
654
  });
603
655
  /**
604
656
  * Agents configuration
@@ -673,6 +725,37 @@ export const TelemetryConfigSchema = z.object({
673
725
  */
674
726
  methodology: MethodologyTelemetryConfigSchema.default(() => MethodologyTelemetryConfigSchema.parse({})),
675
727
  });
728
+ /**
729
+ * WU-1366: Cleanup trigger options
730
+ *
731
+ * Controls when automatic state cleanup runs:
732
+ * - 'on_done': Run after wu:done success (default)
733
+ * - 'on_init': Run during lumenflow init
734
+ * - 'manual': Only run via pnpm state:cleanup
735
+ */
736
+ export const CleanupTriggerSchema = z.enum(['on_done', 'on_init', 'manual']).default('on_done');
737
+ /**
738
+ * WU-1366: Cleanup configuration schema
739
+ *
740
+ * Controls when and how automatic state cleanup runs.
741
+ *
742
+ * @example
743
+ * ```yaml
744
+ * cleanup:
745
+ * trigger: on_done # on_done | on_init | manual
746
+ * ```
747
+ */
748
+ export const CleanupConfigSchema = z.object({
749
+ /**
750
+ * When to trigger automatic state cleanup.
751
+ * - 'on_done': Run after wu:done success (default)
752
+ * - 'on_init': Run during lumenflow init
753
+ * - 'manual': Only run via pnpm state:cleanup
754
+ *
755
+ * @default 'on_done'
756
+ */
757
+ trigger: CleanupTriggerSchema,
758
+ });
676
759
  /**
677
760
  * WU-1345: Lane enforcement configuration schema
678
761
  *
@@ -782,6 +865,17 @@ export const LumenFlowConfigSchema = z.object({
782
865
  agents: AgentsConfigSchema.default(() => AgentsConfigSchema.parse({})),
783
866
  /** Experimental features (WU-1090) */
784
867
  experimental: ExperimentalConfigSchema.default(() => ExperimentalConfigSchema.parse({})),
868
+ /**
869
+ * WU-1366: Cleanup configuration
870
+ * Controls when automatic state cleanup runs.
871
+ *
872
+ * @example
873
+ * ```yaml
874
+ * cleanup:
875
+ * trigger: on_done # on_done | on_init | manual
876
+ * ```
877
+ */
878
+ cleanup: CleanupConfigSchema.default(() => CleanupConfigSchema.parse({})),
785
879
  /**
786
880
  * WU-1270: Telemetry configuration
787
881
  * Opt-in telemetry features for adoption tracking.
@@ -866,7 +960,6 @@ export const LumenFlowConfigSchema = z.object({
866
960
  * @param data - Configuration data to validate
867
961
  * @returns Validation result with parsed config or errors
868
962
  */
869
- // eslint-disable-next-line @typescript-eslint/explicit-function-return-type -- Zod v4 return type inference
870
963
  export function validateConfig(data) {
871
964
  return LumenFlowConfigSchema.safeParse(data);
872
965
  }
@@ -55,7 +55,6 @@ function loadConfigFile(projectRoot) {
55
55
  return data || {};
56
56
  }
57
57
  catch (error) {
58
- // eslint-disable-next-line no-console -- Config loading runs before logger init
59
58
  console.warn(`Warning: Failed to parse ${CONFIG_FILE_NAME}:`, error);
60
59
  return null;
61
60
  }
@@ -105,6 +105,15 @@ export declare const LUMENFLOW_FORCE_ENV = "LUMENFLOW_FORCE";
105
105
  * WU-1081: Exported for use in micro-worktree push operations.
106
106
  */
107
107
  export declare const LUMENFLOW_FORCE_REASON_ENV = "LUMENFLOW_FORCE_REASON";
108
+ /**
109
+ * Environment variable name for LUMENFLOW_WU_TOOL
110
+ *
111
+ * WU-1365: Exported for use by CLI commands that use micro-worktree operations.
112
+ * The pre-push hook checks this env var to allow micro-worktree pushes to main.
113
+ * Valid values are: wu-create, wu-edit, wu-done, wu-delete, wu-claim, wu-block,
114
+ * wu-unblock, initiative-create, initiative-edit, release
115
+ */
116
+ export declare const LUMENFLOW_WU_TOOL_ENV = "LUMENFLOW_WU_TOOL";
108
117
  /**
109
118
  * Default log prefix for micro-worktree operations
110
119
  *
@@ -279,10 +288,24 @@ export declare function cleanupMicroWorktree(worktreePath: string, branchName: s
279
288
  * @returns {Promise<void>}
280
289
  */
281
290
  export declare function stageChangesWithDeletions(gitWorktree: GitAdapter, files: string[] | undefined): Promise<void>;
291
+ /**
292
+ * WU-1365: Check if prettier is available in the project
293
+ *
294
+ * Checks if prettier is installed and executable. Returns false if:
295
+ * - prettier is not in node_modules
296
+ * - pnpm prettier command is not available
297
+ *
298
+ * This allows micro-worktree operations to skip formatting gracefully
299
+ * when prettier is not installed (e.g., in bootstrap or minimal setups).
300
+ *
301
+ * @returns {boolean} True if prettier is available, false otherwise
302
+ */
303
+ export declare function isPrettierAvailable(): boolean;
282
304
  /**
283
305
  * Format files using prettier before committing
284
306
  *
285
307
  * WU-1435: Ensures committed files pass format gates.
308
+ * WU-1365: Gracefully handles missing prettier installation.
286
309
  * Runs prettier --write on specified files within the micro-worktree.
287
310
  *
288
311
  * @param {string[]} files - Relative file paths to format
@@ -77,6 +77,15 @@ export const LUMENFLOW_FORCE_ENV = 'LUMENFLOW_FORCE';
77
77
  * WU-1081: Exported for use in micro-worktree push operations.
78
78
  */
79
79
  export const LUMENFLOW_FORCE_REASON_ENV = 'LUMENFLOW_FORCE_REASON';
80
+ /**
81
+ * Environment variable name for LUMENFLOW_WU_TOOL
82
+ *
83
+ * WU-1365: Exported for use by CLI commands that use micro-worktree operations.
84
+ * The pre-push hook checks this env var to allow micro-worktree pushes to main.
85
+ * Valid values are: wu-create, wu-edit, wu-done, wu-delete, wu-claim, wu-block,
86
+ * wu-unblock, initiative-create, initiative-edit, release
87
+ */
88
+ export const LUMENFLOW_WU_TOOL_ENV = 'LUMENFLOW_WU_TOOL';
80
89
  /**
81
90
  * Default log prefix for micro-worktree operations
82
91
  *
@@ -322,7 +331,6 @@ export async function cleanupOrphanedMicroWorktree(operation, id, gitAdapter, lo
322
331
  */
323
332
  function tryFilesystemCleanup(worktreePath) {
324
333
  try {
325
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool with validated worktree path
326
334
  if (existsSync(worktreePath)) {
327
335
  rmSync(worktreePath, { recursive: true, force: true });
328
336
  }
@@ -372,7 +380,6 @@ export async function cleanupMicroWorktree(worktreePath, branchName, logPrefix =
372
380
  console.log(`${logPrefix} Cleaning up micro-worktree...`);
373
381
  const mainGit = getGitForCwd();
374
382
  // Remove the known worktree path first (must be done before deleting branch)
375
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool with validated worktree path
376
383
  if (existsSync(worktreePath)) {
377
384
  await removeWorktreeSafe(mainGit, worktreePath, logPrefix);
378
385
  }
@@ -443,10 +450,56 @@ export async function stageChangesWithDeletions(gitWorktree, files) {
443
450
  const filesToStage = files || [];
444
451
  await gitWorktree.addWithDeletions(filesToStage);
445
452
  }
453
+ /**
454
+ * WU-1365: Check if prettier is available in the project
455
+ *
456
+ * Checks if prettier is installed and executable. Returns false if:
457
+ * - prettier is not in node_modules
458
+ * - pnpm prettier command is not available
459
+ *
460
+ * This allows micro-worktree operations to skip formatting gracefully
461
+ * when prettier is not installed (e.g., in bootstrap or minimal setups).
462
+ *
463
+ * @returns {boolean} True if prettier is available, false otherwise
464
+ */
465
+ export function isPrettierAvailable() {
466
+ try {
467
+ // Check if prettier is available by running pnpm prettier --version
468
+ // Note: This uses execSync with a known-safe command (no user input)
469
+ execSync(`${PKG_MANAGER} ${SCRIPTS.PRETTIER} --version`, {
470
+ encoding: 'utf-8',
471
+ stdio: STDIO_MODES.PIPE,
472
+ });
473
+ return true;
474
+ }
475
+ catch {
476
+ return false;
477
+ }
478
+ }
479
+ /**
480
+ * WU-1365: Pattern to detect prettier not found errors
481
+ */
482
+ const PRETTIER_NOT_FOUND_PATTERNS = [
483
+ /ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL/,
484
+ /prettier.*not found/i,
485
+ /Cannot find module.*prettier/i,
486
+ /Command failed.*prettier/i,
487
+ /No script.*prettier/i,
488
+ ];
489
+ /**
490
+ * WU-1365: Check if an error indicates prettier is not available
491
+ *
492
+ * @param {string} errMsg - Error message to check
493
+ * @returns {boolean} True if the error indicates prettier is not installed/available
494
+ */
495
+ function isPrettierNotFoundError(errMsg) {
496
+ return PRETTIER_NOT_FOUND_PATTERNS.some((pattern) => pattern.test(errMsg));
497
+ }
446
498
  /**
447
499
  * Format files using prettier before committing
448
500
  *
449
501
  * WU-1435: Ensures committed files pass format gates.
502
+ * WU-1365: Gracefully handles missing prettier installation.
450
503
  * Runs prettier --write on specified files within the micro-worktree.
451
504
  *
452
505
  * @param {string[]} files - Relative file paths to format
@@ -462,7 +515,7 @@ export async function formatFiles(files, worktreePath, logPrefix = DEFAULT_LOG_P
462
515
  const absolutePaths = files.map((f) => join(worktreePath, f));
463
516
  const pathArgs = absolutePaths.map((p) => JSON.stringify(p)).join(' ');
464
517
  try {
465
- // eslint-disable-next-line sonarjs/os-command -- CLI tool executing known safe prettier command with validated paths
518
+ // Note: This uses execSync with validated paths (built from worktreePath and file list)
466
519
  execSync(`${PKG_MANAGER} ${SCRIPTS.PRETTIER} ${PRETTIER_FLAGS.WRITE} ${pathArgs}`, {
467
520
  encoding: 'utf-8',
468
521
  stdio: STDIO_MODES.PIPE,
@@ -471,8 +524,15 @@ export async function formatFiles(files, worktreePath, logPrefix = DEFAULT_LOG_P
471
524
  console.log(`${logPrefix} ✅ Files formatted`);
472
525
  }
473
526
  catch (err) {
474
- // Log warning but don't fail - some files may not need formatting
475
527
  const errMsg = err instanceof Error ? err.message : String(err);
528
+ // WU-1365: Check if the error is due to prettier not being available
529
+ if (isPrettierNotFoundError(errMsg)) {
530
+ console.warn(`${logPrefix} ⚠️ Skipping formatting: prettier not available.\n` +
531
+ ` To enable formatting, install prettier: pnpm add -D prettier\n` +
532
+ ` Files will be committed without formatting.`);
533
+ return;
534
+ }
535
+ // Log warning but don't fail - some files may not need formatting
476
536
  console.warn(`${logPrefix} ⚠️ Formatting warning: ${errMsg}`);
477
537
  }
478
538
  }
@@ -538,7 +598,6 @@ export async function mergeWithRetry(tempBranchName, microWorktreePath, logPrefi
538
598
  * @throws {Error} If push fails after all retries
539
599
  */
540
600
  export async function pushWithRetry(mainGit, worktreeGit, remote, branch, tempBranchName, logPrefix = DEFAULT_LOG_PREFIX) {
541
- // eslint-disable-next-line sonarjs/deprecation -- Using deprecated constant for backwards compatibility
542
601
  const maxRetries = MAX_PUSH_RETRIES;
543
602
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
544
603
  try {
@@ -58,7 +58,6 @@ export function containsPnpmDependencyCommand(command) {
58
58
  if (!command || typeof command !== 'string') {
59
59
  return false;
60
60
  }
61
- // eslint-disable-next-line security/detect-non-literal-regexp -- Pattern built from const array, not user input
62
61
  const pattern = new RegExp(`pnpm\\s+(${DEPENDENCY_COMMANDS.join('|')})\\b`, 'i');
63
62
  return pattern.test(command);
64
63
  }
@@ -7,7 +7,6 @@
7
7
  *
8
8
  * @see {@link packages/@lumenflow/cli/src/wu-done.ts} - Consumer (rollbackTransaction function)
9
9
  */
10
- /* eslint-disable security/detect-non-literal-fs-filename */
11
10
  import { writeFileSync, unlinkSync } from 'node:fs';
12
11
  /**
13
12
  * Result of a rollback operation.
@@ -9,7 +9,6 @@
9
9
  * Stamp files (.lumenflow/stamps/WU-{id}.done) serve as completion markers
10
10
  * Used by wu:done, wu:recovery, and validation tools
11
11
  */
12
- /* eslint-disable security/detect-non-literal-fs-filename */
13
12
  import { existsSync, writeFileSync, mkdirSync } from 'node:fs';
14
13
  import { readFile, access } from 'node:fs/promises';
15
14
  import { constants } from 'node:fs';
@@ -74,7 +74,6 @@ function checkSymlinkTarget(linkTarget, basePath) {
74
74
  ? linkTarget
75
75
  : path.resolve(basePath, linkTarget);
76
76
  const isWorktreePath = absoluteTarget.includes(WORKTREES_PATH_SEGMENT);
77
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- validated path from symlink
78
77
  const isBroken = isWorktreePath && !fs.existsSync(absoluteTarget);
79
78
  return { isWorktreePath, absoluteTarget, isBroken };
80
79
  }
@@ -86,7 +85,6 @@ function checkSymlinkTarget(linkTarget, basePath) {
86
85
  * @param {{hasWorktreeSymlinks: boolean, brokenSymlinks: string[]}} result - Result object to mutate
87
86
  */
88
87
  function processSymlinkEntry(entryPath, basePath, result) {
89
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- validated path from directory scan
90
88
  const linkTarget = fs.readlinkSync(entryPath);
91
89
  const check = checkSymlinkTarget(linkTarget, basePath);
92
90
  if (check.isWorktreePath) {
@@ -105,7 +103,6 @@ function processSymlinkEntry(entryPath, basePath, result) {
105
103
  */
106
104
  function scanPnpmForWorktreeSymlinks(pnpmPath) {
107
105
  const result = { hasWorktreeSymlinks: false, brokenSymlinks: [] };
108
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- validated path
109
106
  const entries = fs.readdirSync(pnpmPath, { withFileTypes: true });
110
107
  for (const entry of entries) {
111
108
  if (entry.isSymbolicLink()) {
@@ -127,11 +124,9 @@ function scanPnpmForWorktreeSymlinks(pnpmPath) {
127
124
  */
128
125
  export function hasWorktreePathSymlinks(nodeModulesPath) {
129
126
  const result = { hasWorktreeSymlinks: false, brokenSymlinks: [] };
130
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- validated path
131
127
  if (!fs.existsSync(nodeModulesPath)) {
132
128
  return result;
133
129
  }
134
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- validated path
135
130
  const entries = fs.readdirSync(nodeModulesPath, { withFileTypes: true });
136
131
  for (const entry of entries) {
137
132
  const entryPath = path.join(nodeModulesPath, entry.name);
@@ -156,7 +151,6 @@ export function hasWorktreePathSymlinks(nodeModulesPath) {
156
151
  */
157
152
  function nodeModulesExists(targetPath) {
158
153
  try {
159
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- validated worktree path
160
154
  fs.lstatSync(targetPath);
161
155
  return true;
162
156
  }
@@ -219,7 +213,6 @@ export function symlinkNodeModules(worktreePath, logger = console, mainRepoPath
219
213
  }
220
214
  }
221
215
  try {
222
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- validated worktree path
223
216
  fs.symlinkSync(RELATIVE_NODE_MODULES_PATH, targetPath);
224
217
  if (logger.info) {
225
218
  logger.info(`${LOG_PREFIX.CLAIM} node_modules symlink created -> ${RELATIVE_NODE_MODULES_PATH}`);
@@ -241,11 +234,9 @@ export function symlinkNodeModules(worktreePath, logger = console, mainRepoPath
241
234
  * @returns {boolean} True if should skip
242
235
  */
243
236
  function shouldSkipNestedPackage(targetDir, sourceNodeModules) {
244
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- validated paths
245
237
  if (!fs.existsSync(targetDir)) {
246
238
  return true;
247
239
  }
248
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- validated paths
249
240
  if (!fs.existsSync(sourceNodeModules)) {
250
241
  return true;
251
242
  }
@@ -263,7 +254,6 @@ function shouldSkipNestedPackage(targetDir, sourceNodeModules) {
263
254
  function handleExistingNestedNodeModules(targetNodeModules, pkgPath, logger, errors) {
264
255
  let targetStat;
265
256
  try {
266
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- validated paths
267
257
  targetStat = fs.lstatSync(targetNodeModules);
268
258
  }
269
259
  catch {
@@ -278,7 +268,6 @@ function handleExistingNestedNodeModules(targetNodeModules, pkgPath, logger, err
278
268
  return 'skip';
279
269
  }
280
270
  // Check if directory has meaningful content
281
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- validated paths
282
271
  const contents = fs.readdirSync(targetNodeModules);
283
272
  const hasMeaningfulContent = contents.some((item) => !item.startsWith('.') && item !== '.vite' && item !== '.turbo');
284
273
  if (hasMeaningfulContent) {
@@ -335,7 +324,6 @@ export function symlinkNestedNodeModules(worktreePath, mainRepoPath, logger = nu
335
324
  }
336
325
  try {
337
326
  const relativePath = path.relative(targetDir, sourceNodeModules);
338
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- validated paths
339
327
  fs.symlinkSync(relativePath, targetNodeModules);
340
328
  created++;
341
329
  if (logger?.info) {
@@ -102,6 +102,11 @@ interface ConsistencyError {
102
102
  * in a micro-worktree, then committed and pushed to origin/main.
103
103
  * This prevents direct writes to the main checkout.
104
104
  *
105
+ * WU-1370: When projectRoot is explicitly provided (not process.cwd()), the caller
106
+ * is already inside a micro-worktree context (e.g., handleOrphanCheck during wu:claim).
107
+ * In this case, skip creating a nested micro-worktree and work directly in projectRoot.
108
+ * This prevents local main drift from nested micro-worktrees merging before pushing.
109
+ *
105
110
  * @param {object} report - Report from checkWUConsistency()
106
111
  * @param {RepairWUInconsistencyOptions} [options={}] - Repair options
107
112
  * @returns {Promise<object>} Result with repaired, skipped, and failed counts
@@ -271,12 +271,21 @@ function categorizeErrors(errors) {
271
271
  * in a micro-worktree, then committed and pushed to origin/main.
272
272
  * This prevents direct writes to the main checkout.
273
273
  *
274
+ * WU-1370: When projectRoot is explicitly provided (not process.cwd()), the caller
275
+ * is already inside a micro-worktree context (e.g., handleOrphanCheck during wu:claim).
276
+ * In this case, skip creating a nested micro-worktree and work directly in projectRoot.
277
+ * This prevents local main drift from nested micro-worktrees merging before pushing.
278
+ *
274
279
  * @param {object} report - Report from checkWUConsistency()
275
280
  * @param {RepairWUInconsistencyOptions} [options={}] - Repair options
276
281
  * @returns {Promise<object>} Result with repaired, skipped, and failed counts
277
282
  */
278
283
  export async function repairWUInconsistency(report, options = {}) {
279
- const { dryRun = false, projectRoot = process.cwd() } = options;
284
+ const { dryRun = false, projectRoot } = options;
285
+ // WU-1370: Detect if projectRoot was explicitly provided
286
+ // If provided, we're inside a micro-worktree and should work directly in projectRoot
287
+ const isInsideMicroWorktree = projectRoot !== undefined;
288
+ const effectiveProjectRoot = projectRoot ?? process.cwd();
280
289
  if (report.valid) {
281
290
  return { repaired: 0, skipped: 0, failed: 0 };
282
291
  }
@@ -292,60 +301,93 @@ export async function repairWUInconsistency(report, options = {}) {
292
301
  failed: 0,
293
302
  };
294
303
  }
295
- // Step 1: Process file-based repairs via micro-worktree (batched)
304
+ // Step 1: Process file-based repairs
296
305
  if (fileRepairs.length > 0) {
297
- try {
298
- // Generate a batch ID from the WU IDs being repaired
299
- const batchId = `batch-${fileRepairs.map((e) => e.wuId).join('-')}`.slice(0, 50);
300
- await withMicroWorktree({
301
- operation: 'wu-repair',
302
- id: batchId,
303
- logPrefix: LOG_PREFIX.REPAIR,
304
- execute: async ({ worktreePath }) => {
305
- const filesModified = [];
306
- for (const error of fileRepairs) {
307
- try {
308
- const result = await repairSingleErrorInWorktree(error, worktreePath, projectRoot);
309
- if (result.success && result.files) {
310
- filesModified.push(...result.files);
311
- repaired++;
312
- }
313
- else if (result.skipped) {
314
- skipped++;
315
- if (result.reason) {
316
- console.warn(`${LOG_PREFIX.REPAIR} Skipped ${error.type}: ${result.reason}`);
306
+ // WU-1370: When projectRoot is provided, we're already in a micro-worktree context
307
+ // (e.g., called from handleOrphanCheck during wu:claim). Work directly in projectRoot
308
+ // instead of creating a nested micro-worktree.
309
+ if (isInsideMicroWorktree) {
310
+ // Direct repair mode: work in the provided projectRoot
311
+ for (const error of fileRepairs) {
312
+ try {
313
+ // When inside a micro-worktree, worktreePath === projectRoot
314
+ // We're both reading from and writing to the same location
315
+ const result = await repairSingleErrorInWorktree(error, effectiveProjectRoot, effectiveProjectRoot);
316
+ if (result.success && result.files) {
317
+ repaired++;
318
+ }
319
+ else if (result.skipped) {
320
+ skipped++;
321
+ if (result.reason) {
322
+ console.warn(`${LOG_PREFIX.REPAIR} Skipped ${error.type}: ${result.reason}`);
323
+ }
324
+ }
325
+ else {
326
+ failed++;
327
+ }
328
+ }
329
+ catch (err) {
330
+ const errMessage = err instanceof Error ? err.message : String(err);
331
+ console.error(`${LOG_PREFIX.REPAIR} Failed to repair ${error.type}: ${errMessage}`);
332
+ failed++;
333
+ }
334
+ }
335
+ }
336
+ else {
337
+ // Standard mode: create micro-worktree for isolation
338
+ try {
339
+ // Generate a batch ID from the WU IDs being repaired
340
+ const batchId = `batch-${fileRepairs.map((e) => e.wuId).join('-')}`.slice(0, 50);
341
+ await withMicroWorktree({
342
+ operation: 'wu-repair',
343
+ id: batchId,
344
+ logPrefix: LOG_PREFIX.REPAIR,
345
+ execute: async ({ worktreePath }) => {
346
+ const filesModified = [];
347
+ for (const error of fileRepairs) {
348
+ try {
349
+ const result = await repairSingleErrorInWorktree(error, worktreePath, effectiveProjectRoot);
350
+ if (result.success && result.files) {
351
+ filesModified.push(...result.files);
352
+ repaired++;
353
+ }
354
+ else if (result.skipped) {
355
+ skipped++;
356
+ if (result.reason) {
357
+ console.warn(`${LOG_PREFIX.REPAIR} Skipped ${error.type}: ${result.reason}`);
358
+ }
359
+ }
360
+ else {
361
+ failed++;
317
362
  }
318
363
  }
319
- else {
364
+ catch (err) {
365
+ const errMessage = err instanceof Error ? err.message : String(err);
366
+ console.error(`${LOG_PREFIX.REPAIR} Failed to repair ${error.type}: ${errMessage}`);
320
367
  failed++;
321
368
  }
322
369
  }
323
- catch (err) {
324
- const errMessage = err instanceof Error ? err.message : String(err);
325
- console.error(`${LOG_PREFIX.REPAIR} Failed to repair ${error.type}: ${errMessage}`);
326
- failed++;
327
- }
328
- }
329
- // Deduplicate files
330
- const uniqueFiles = [...new Set(filesModified)];
331
- return {
332
- commitMessage: `fix: repair ${repaired} WU inconsistencies`,
333
- files: uniqueFiles,
334
- };
335
- },
336
- });
337
- }
338
- catch (err) {
339
- // If micro-worktree fails, mark all file repairs as failed
340
- const errMessage = err instanceof Error ? err.message : String(err);
341
- console.error(`${LOG_PREFIX.REPAIR} Micro-worktree operation failed: ${errMessage}`);
342
- failed += fileRepairs.length - repaired;
370
+ // Deduplicate files
371
+ const uniqueFiles = [...new Set(filesModified)];
372
+ return {
373
+ commitMessage: `fix: repair ${repaired} WU inconsistencies`,
374
+ files: uniqueFiles,
375
+ };
376
+ },
377
+ });
378
+ }
379
+ catch (err) {
380
+ // If micro-worktree fails, mark all file repairs as failed
381
+ const errMessage = err instanceof Error ? err.message : String(err);
382
+ console.error(`${LOG_PREFIX.REPAIR} Micro-worktree operation failed: ${errMessage}`);
383
+ failed += fileRepairs.length - repaired;
384
+ }
343
385
  }
344
386
  }
345
387
  // Step 2: Process git-only repairs (worktree/branch cleanup) directly
346
388
  for (const error of gitOnlyRepairs) {
347
389
  try {
348
- const result = await repairGitOnlyError(error, projectRoot);
390
+ const result = await repairGitOnlyError(error, effectiveProjectRoot);
349
391
  if (result.success) {
350
392
  repaired++;
351
393
  }
@@ -1400,7 +1400,6 @@ export const PATH_LITERALS = {
1400
1400
  /** Plan file suffix for WU plans */
1401
1401
  PLAN_FILE_SUFFIX: '-plan.md',
1402
1402
  /** Trailing slash regex pattern */
1403
- // eslint-disable-next-line sonarjs/slow-regex -- False positive: simple end-anchor regex, no catastrophic backtracking possible
1404
1403
  TRAILING_SLASH_REGEX: /\/+$/,
1405
1404
  };
1406
1405
  /**
@@ -590,7 +590,6 @@ const WU_EVENTS_PATH = path.join('.lumenflow', 'state', WU_EVENTS_FILE_NAME);
590
590
  function normalizeEventForKey(event) {
591
591
  const normalized = {};
592
592
  for (const key of Object.keys(event).sort()) {
593
- // eslint-disable-next-line security/detect-object-injection -- keys derived from object keys
594
593
  normalized[key] = event[key];
595
594
  }
596
595
  return normalized;
@@ -141,7 +141,6 @@ export function shouldArchiveEvent(event, config, context) {
141
141
  async function loadAllEvents(baseDir) {
142
142
  const eventsPath = path.join(baseDir, LUMENFLOW_PATHS.STATE_DIR, WU_EVENTS_FILE_NAME);
143
143
  try {
144
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool reads known path
145
144
  const content = await fs.readFile(eventsPath, { encoding: 'utf-8' });
146
145
  const lines = content.split('\n').filter((line) => line.trim());
147
146
  return lines.map((line) => {
@@ -311,7 +310,6 @@ async function appendToArchive(baseDir, archivePath, events) {
311
310
  mkdirSync(dirPath, { recursive: true });
312
311
  const content = events.map((e) => JSON.stringify(e)).join('\n') + '\n';
313
312
  // Append to archive file (creates if doesn't exist)
314
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool writes known path
315
313
  await fs.appendFile(fullPath, content, 'utf-8');
316
314
  }
317
315
  /**
@@ -18,7 +18,6 @@
18
18
  * - Clear error messages pointing to missing files
19
19
  * - Reusable by wu:done for early validation
20
20
  */
21
- /* eslint-disable security/detect-non-literal-fs-filename, security/detect-object-injection */
22
21
  import { existsSync } from 'node:fs';
23
22
  import path from 'node:path';
24
23
  import { WU_PATHS } from './wu-paths.js';
@@ -184,7 +183,8 @@ export async function validatePreflight(id, options = {}) {
184
183
  const wuPath = path.join(worktreePath, WU_PATHS.WU(id));
185
184
  // Debug logging for YAML source (WU-1830)
186
185
  if (options.worktreePath && options.worktreePath !== rootDir) {
187
- process.env.DEBUG && console.log(`[wu-preflight] Reading WU YAML from worktree: ${wuPath}`);
186
+ if (process.env.DEBUG)
187
+ console.log(`[wu-preflight] Reading WU YAML from worktree: ${wuPath}`);
188
188
  }
189
189
  let doc;
190
190
  try {
@@ -224,7 +224,8 @@ export async function validatePreflight(id, options = {}) {
224
224
  suggestedTestPaths = await findSuggestedTestPaths(missingTestPaths, searchRoot);
225
225
  }
226
226
  catch (err) {
227
- process.env.DEBUG && console.log(`[wu-preflight] Failed to find suggestions: ${err.message}`);
227
+ if (process.env.DEBUG)
228
+ console.log(`[wu-preflight] Failed to find suggestions: ${err.message}`);
228
229
  }
229
230
  }
230
231
  return createPreflightResult({
package/dist/wu-schema.js CHANGED
@@ -860,7 +860,6 @@ function detectNormalizationChanges(original, normalized) {
860
860
  return true;
861
861
  }
862
862
  for (let i = 0; i < origPaths.length; i++) {
863
- // eslint-disable-next-line security/detect-object-injection
864
863
  if (origPaths[i] !== normPaths[i]) {
865
864
  return true;
866
865
  }
@@ -871,7 +870,6 @@ function detectNormalizationChanges(original, normalized) {
871
870
  return true;
872
871
  }
873
872
  for (let i = 0; i < original.acceptance.length; i++) {
874
- // eslint-disable-next-line security/detect-object-injection
875
873
  if (original.acceptance[i] !== normalized.acceptance[i]) {
876
874
  return true;
877
875
  }
@@ -6,7 +6,6 @@
6
6
  *
7
7
  * Used by both main wu:done flow AND recovery mode (DRY principle)
8
8
  */
9
- /* eslint-disable security/detect-non-literal-fs-filename, security/detect-object-injection */
10
9
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
11
10
  import { parseBacklogFrontmatter } from './backlog-parser.js';
12
11
  import { getSectionHeadingsWithDefaults } from './section-headings.js';
@@ -22,7 +22,6 @@
22
22
  * tx.commit();
23
23
  * ```
24
24
  */
25
- /* eslint-disable security/detect-non-literal-fs-filename, security/detect-object-injection */
26
25
  import { existsSync, readFileSync } from 'node:fs';
27
26
  import { stringifyYAML } from './wu-yaml.js';
28
27
  import { parseBacklogFrontmatter } from './backlog-parser.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumenflow/core",
3
- "version": "2.7.0",
3
+ "version": "2.9.0",
4
4
  "description": "Core WU lifecycle tools for LumenFlow workflow framework",
5
5
  "keywords": [
6
6
  "lumenflow",
@@ -99,7 +99,7 @@
99
99
  "vitest": "^4.0.17"
100
100
  },
101
101
  "peerDependencies": {
102
- "@lumenflow/memory": "2.7.0"
102
+ "@lumenflow/memory": "2.9.0"
103
103
  },
104
104
  "peerDependenciesMeta": {
105
105
  "@lumenflow/memory": {