@lumenflow/core 2.7.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.
- package/dist/backlog-editor.js +0 -1
- package/dist/backlog-generator.js +1 -1
- package/dist/cleanup-lock.js +0 -1
- package/dist/code-path-validator.js +0 -1
- package/dist/commands-logger.js +0 -1
- package/dist/compliance-parser.js +1 -1
- package/dist/gates-config.js +0 -4
- package/dist/git-adapter.js +0 -2
- package/dist/git-context-extractor.js +0 -3
- package/dist/lane-checker.js +0 -6
- package/dist/lane-lock.js +0 -1
- package/dist/lumenflow-config-schema.d.ts +100 -0
- package/dist/lumenflow-config-schema.js +94 -1
- package/dist/lumenflow-config.js +0 -1
- package/dist/micro-worktree.d.ts +23 -0
- package/dist/micro-worktree.js +64 -5
- package/dist/piped-command-detector.js +0 -1
- package/dist/rollback-utils.js +0 -1
- package/dist/stamp-utils.js +0 -1
- package/dist/worktree-symlink.js +0 -12
- package/dist/wu-consistency-checker.d.ts +5 -0
- package/dist/wu-consistency-checker.js +86 -44
- package/dist/wu-constants.js +0 -1
- package/dist/wu-done-worktree.js +0 -1
- package/dist/wu-events-cleanup.js +0 -2
- package/dist/wu-preflight-validators.js +4 -3
- package/dist/wu-schema.js +0 -2
- package/dist/wu-status-updater.js +0 -1
- package/dist/wu-transaction-collectors.js +0 -1
- package/package.json +2 -2
package/dist/backlog-editor.js
CHANGED
|
@@ -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
|
|
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
|
|
package/dist/cleanup-lock.js
CHANGED
|
@@ -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';
|
package/dist/commands-logger.js
CHANGED
|
@@ -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
|
package/dist/gates-config.js
CHANGED
|
@@ -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
|
}
|
package/dist/git-adapter.js
CHANGED
|
@@ -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',
|
package/dist/lane-checker.js
CHANGED
|
@@ -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
|
}
|
package/dist/lumenflow-config.js
CHANGED
|
@@ -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
|
}
|
package/dist/micro-worktree.d.ts
CHANGED
|
@@ -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
|
package/dist/micro-worktree.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
}
|
package/dist/rollback-utils.js
CHANGED
|
@@ -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.
|
package/dist/stamp-utils.js
CHANGED
|
@@ -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';
|
package/dist/worktree-symlink.js
CHANGED
|
@@ -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
|
|
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
|
|
304
|
+
// Step 1: Process file-based repairs
|
|
296
305
|
if (fileRepairs.length > 0) {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
-
|
|
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
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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,
|
|
390
|
+
const result = await repairGitOnlyError(error, effectiveProjectRoot);
|
|
349
391
|
if (result.success) {
|
|
350
392
|
repaired++;
|
|
351
393
|
}
|
package/dist/wu-constants.js
CHANGED
|
@@ -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
|
/**
|
package/dist/wu-done-worktree.js
CHANGED
|
@@ -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
|
|
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
|
|
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.
|
|
3
|
+
"version": "2.8.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.
|
|
102
|
+
"@lumenflow/memory": "2.8.0"
|
|
103
103
|
},
|
|
104
104
|
"peerDependenciesMeta": {
|
|
105
105
|
"@lumenflow/memory": {
|