@lumenflow/core 2.4.0 → 2.5.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/arg-parser.d.ts +7 -0
- package/dist/arg-parser.js +11 -1
- package/dist/lane-checker.d.ts +29 -2
- package/dist/lane-checker.js +176 -20
- package/dist/lane-lock.d.ts +8 -0
- package/dist/lane-lock.js +21 -0
- package/dist/lumenflow-config-schema.d.ts +83 -0
- package/dist/lumenflow-config-schema.js +112 -0
- package/dist/lumenflow-config.d.ts +3 -1
- package/dist/lumenflow-config.js +9 -0
- package/dist/micro-worktree.d.ts +151 -0
- package/dist/micro-worktree.js +308 -9
- package/dist/wu-constants.d.ts +8 -0
- package/dist/wu-constants.js +8 -0
- package/dist/wu-done-concurrent-merge.d.ts +13 -0
- package/dist/wu-done-concurrent-merge.js +41 -1
- package/dist/wu-done-metadata.js +3 -3
- package/dist/wu-paths.d.ts +20 -0
- package/dist/wu-paths.js +10 -0
- package/dist/wu-preflight-validators.d.ts +30 -0
- package/dist/wu-preflight-validators.js +6 -2
- package/dist/wu-transaction-collectors.d.ts +13 -0
- package/dist/wu-transaction-collectors.js +20 -32
- package/package.json +3 -2
package/dist/arg-parser.d.ts
CHANGED
|
@@ -32,6 +32,13 @@ export declare const WU_OPTIONS: Record<string, WUOption>;
|
|
|
32
32
|
* These options control how wu:create handles external plan storage.
|
|
33
33
|
*/
|
|
34
34
|
export declare const WU_CREATE_OPTIONS: Record<string, WUOption>;
|
|
35
|
+
/**
|
|
36
|
+
* Negated options that commander handles specially.
|
|
37
|
+
* --no-foo creates opts.foo = false. We convert to noFoo = true.
|
|
38
|
+
*
|
|
39
|
+
* WU-1329: Export for testing purposes.
|
|
40
|
+
*/
|
|
41
|
+
export declare const NEGATED_OPTIONS: string[];
|
|
35
42
|
/**
|
|
36
43
|
* Create a commander-based CLI parser for a WU script.
|
|
37
44
|
*
|
package/dist/arg-parser.js
CHANGED
|
@@ -438,6 +438,14 @@ export const WU_OPTIONS = {
|
|
|
438
438
|
flags: '--skip-setup',
|
|
439
439
|
description: 'Skip automatic pnpm install in worktree after creation (faster claims when deps already built)',
|
|
440
440
|
},
|
|
441
|
+
// WU-1329: Strict validation options
|
|
442
|
+
// NOTE: --no-strict is the opt-out flag; strict is the default behavior
|
|
443
|
+
noStrict: {
|
|
444
|
+
name: 'noStrict',
|
|
445
|
+
flags: '--no-strict',
|
|
446
|
+
description: 'Bypass strict validation (skip code_paths/test_paths existence checks, treat warnings as advisory). Logged when used.',
|
|
447
|
+
isNegated: true,
|
|
448
|
+
},
|
|
441
449
|
};
|
|
442
450
|
/**
|
|
443
451
|
* WU-1062: Additional options for wu:create command
|
|
@@ -458,8 +466,10 @@ export const WU_CREATE_OPTIONS = {
|
|
|
458
466
|
/**
|
|
459
467
|
* Negated options that commander handles specially.
|
|
460
468
|
* --no-foo creates opts.foo = false. We convert to noFoo = true.
|
|
469
|
+
*
|
|
470
|
+
* WU-1329: Export for testing purposes.
|
|
461
471
|
*/
|
|
462
|
-
const NEGATED_OPTIONS = ['auto', 'remove', 'merge', 'autoRebase', 'push'];
|
|
472
|
+
export const NEGATED_OPTIONS = ['auto', 'remove', 'merge', 'autoRebase', 'push', 'strict'];
|
|
463
473
|
/**
|
|
464
474
|
* Post-process commander opts to handle negated boolean options.
|
|
465
475
|
* Commander's --no-* flags create opts.foo = false.
|
package/dist/lane-checker.d.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* Used by wu-claim.ts and wu-unblock.ts to prevent WIP violations.
|
|
7
7
|
*/
|
|
8
8
|
import { getSubLanesForParent } from './lane-inference.js';
|
|
9
|
+
import type { LockPolicy } from './lumenflow-config-schema.js';
|
|
9
10
|
interface ValidateLaneOptions {
|
|
10
11
|
strict?: boolean;
|
|
11
12
|
}
|
|
@@ -61,13 +62,39 @@ export declare function validateLaneFormat(lane: string, configPath?: string | n
|
|
|
61
62
|
* @returns {number} The WIP limit for the lane (default: 1)
|
|
62
63
|
*/
|
|
63
64
|
export declare function getWipLimitForLane(lane: string, options?: GetWipLimitOptions): number;
|
|
65
|
+
/** WU-1325: Options for getLockPolicyForLane */
|
|
66
|
+
interface GetLockPolicyOptions {
|
|
67
|
+
/** Path to .lumenflow.config.yaml (for testing) */
|
|
68
|
+
configPath?: string;
|
|
69
|
+
}
|
|
64
70
|
/**
|
|
65
|
-
*
|
|
71
|
+
* WU-1325: Get lock policy for a lane from config
|
|
72
|
+
*
|
|
73
|
+
* Reads the lock_policy field from .lumenflow.config.yaml for the specified lane.
|
|
74
|
+
* Returns DEFAULT_LOCK_POLICY ('all') if the lane is not found or lock_policy is not specified.
|
|
75
|
+
*
|
|
76
|
+
* Lock policies:
|
|
77
|
+
* - 'all' (default): Lock held through entire WU lifecycle (claim to done)
|
|
78
|
+
* - 'active': Lock released on block, re-acquired on unblock
|
|
79
|
+
* - 'none': No lock files created, WIP checking disabled for this lane
|
|
80
|
+
*
|
|
81
|
+
* @param {string} lane - Lane name (e.g., "Framework: Core", "Content: Documentation")
|
|
82
|
+
* @param {GetLockPolicyOptions} options - Options including configPath for testing
|
|
83
|
+
* @returns {LockPolicy} The lock policy for the lane (default: 'all')
|
|
84
|
+
*/
|
|
85
|
+
export declare function getLockPolicyForLane(lane: string, options?: GetLockPolicyOptions): LockPolicy;
|
|
86
|
+
/**
|
|
87
|
+
* Check if a lane is free (WU count is below wip_limit)
|
|
66
88
|
*
|
|
67
89
|
* WU-1016: Now respects configurable wip_limit per lane from .lumenflow.config.yaml.
|
|
68
|
-
* Lane is considered "free" if current
|
|
90
|
+
* Lane is considered "free" if current WU count < wip_limit.
|
|
69
91
|
* Default wip_limit is 1 if not specified in config (backward compatible).
|
|
70
92
|
*
|
|
93
|
+
* WU-1324: Now respects lock_policy for WIP counting:
|
|
94
|
+
* - 'all' (default): Count in_progress + blocked WUs toward WIP limit
|
|
95
|
+
* - 'active': Count only in_progress WUs (blocked WUs release lane lock)
|
|
96
|
+
* - 'none': Disable WIP checking entirely (lane always free)
|
|
97
|
+
*
|
|
71
98
|
* @param {string} statusPath - Path to status.md
|
|
72
99
|
* @param {string} lane - Lane name (e.g., "Operations", "Intelligence")
|
|
73
100
|
* @param {string} wuid - WU ID being claimed (e.g., "WU-419")
|
package/dist/lane-checker.js
CHANGED
|
@@ -35,6 +35,15 @@ export function extractParent(lane) {
|
|
|
35
35
|
// Sub-lane format: extract parent before colon
|
|
36
36
|
return trimmed.substring(0, colonIndex).trim();
|
|
37
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* WU-1308: Check if lane-inference.yaml file exists
|
|
40
|
+
* @returns {boolean} True if the file exists
|
|
41
|
+
*/
|
|
42
|
+
function laneInferenceFileExists() {
|
|
43
|
+
const projectRoot = findProjectRoot();
|
|
44
|
+
const taxonomyPath = path.join(projectRoot, CONFIG_FILES.LANE_INFERENCE);
|
|
45
|
+
return existsSync(taxonomyPath);
|
|
46
|
+
}
|
|
38
47
|
/**
|
|
39
48
|
* Check if a parent lane has sub-lane taxonomy defined
|
|
40
49
|
* @param {string} parent - Parent lane name
|
|
@@ -141,6 +150,17 @@ function validateSubLaneFormat(lane, trimmed, colonIndex, configPath) {
|
|
|
141
150
|
if (!isValidParentLane(parent, configPath)) {
|
|
142
151
|
throw createError(ErrorCodes.INVALID_LANE, `Unknown parent lane: "${parent}". Check ${CONFIG_FILES.LUMENFLOW_CONFIG} for valid lanes.`, { parent, lane });
|
|
143
152
|
}
|
|
153
|
+
// WU-1308: Check if lane-inference file exists before validating sub-lanes
|
|
154
|
+
// This provides a clear error message when the file is missing
|
|
155
|
+
if (!laneInferenceFileExists()) {
|
|
156
|
+
throw createError(ErrorCodes.FILE_NOT_FOUND, `Sub-lane validation requires ${CONFIG_FILES.LANE_INFERENCE} which is missing.\n\n` +
|
|
157
|
+
`The file "${CONFIG_FILES.LANE_INFERENCE}" defines the lane taxonomy for sub-lane validation.\n\n` +
|
|
158
|
+
`To fix this:\n` +
|
|
159
|
+
` 1. Generate a lane taxonomy from your codebase:\n` +
|
|
160
|
+
` pnpm lane:suggest --output ${CONFIG_FILES.LANE_INFERENCE}\n\n` +
|
|
161
|
+
` 2. Or copy from an example project and customize.\n\n` +
|
|
162
|
+
`See: LUMENFLOW.md "Setup Notes" section for details.`, { lane, parent, subdomain, missingFile: CONFIG_FILES.LANE_INFERENCE });
|
|
163
|
+
}
|
|
144
164
|
// Validate sub-lane exists in taxonomy
|
|
145
165
|
if (hasSubLaneTaxonomy(parent)) {
|
|
146
166
|
validateSubLaneInTaxonomy(parent, subdomain);
|
|
@@ -342,6 +362,78 @@ export function getWipLimitForLane(lane, options = {}) {
|
|
|
342
362
|
return DEFAULT_WIP_LIMIT;
|
|
343
363
|
}
|
|
344
364
|
}
|
|
365
|
+
/** WU-1325: Default lock policy when not specified in config */
|
|
366
|
+
const DEFAULT_LOCK_POLICY = 'all';
|
|
367
|
+
/**
|
|
368
|
+
* WU-1325: Get lock policy for a lane from config
|
|
369
|
+
*
|
|
370
|
+
* Reads the lock_policy field from .lumenflow.config.yaml for the specified lane.
|
|
371
|
+
* Returns DEFAULT_LOCK_POLICY ('all') if the lane is not found or lock_policy is not specified.
|
|
372
|
+
*
|
|
373
|
+
* Lock policies:
|
|
374
|
+
* - 'all' (default): Lock held through entire WU lifecycle (claim to done)
|
|
375
|
+
* - 'active': Lock released on block, re-acquired on unblock
|
|
376
|
+
* - 'none': No lock files created, WIP checking disabled for this lane
|
|
377
|
+
*
|
|
378
|
+
* @param {string} lane - Lane name (e.g., "Framework: Core", "Content: Documentation")
|
|
379
|
+
* @param {GetLockPolicyOptions} options - Options including configPath for testing
|
|
380
|
+
* @returns {LockPolicy} The lock policy for the lane (default: 'all')
|
|
381
|
+
*/
|
|
382
|
+
export function getLockPolicyForLane(lane, options = {}) {
|
|
383
|
+
// Determine config path
|
|
384
|
+
let resolvedConfigPath = options.configPath;
|
|
385
|
+
if (!resolvedConfigPath) {
|
|
386
|
+
const projectRoot = findProjectRoot();
|
|
387
|
+
resolvedConfigPath = path.join(projectRoot, CONFIG_FILES.LUMENFLOW_CONFIG);
|
|
388
|
+
}
|
|
389
|
+
// Check if config file exists
|
|
390
|
+
if (!existsSync(resolvedConfigPath)) {
|
|
391
|
+
return DEFAULT_LOCK_POLICY;
|
|
392
|
+
}
|
|
393
|
+
try {
|
|
394
|
+
const configContent = readFileSync(resolvedConfigPath, { encoding: 'utf-8' });
|
|
395
|
+
const config = parseYAML(configContent);
|
|
396
|
+
if (!config.lanes) {
|
|
397
|
+
return DEFAULT_LOCK_POLICY;
|
|
398
|
+
}
|
|
399
|
+
// Normalize lane name for case-insensitive comparison
|
|
400
|
+
const normalizedLane = lane.toLowerCase().trim();
|
|
401
|
+
// Extract all lanes with their lock_policy
|
|
402
|
+
let allLanes = [];
|
|
403
|
+
if (Array.isArray(config.lanes)) {
|
|
404
|
+
// Flat array format: lanes: [{name: "Core", lock_policy: "none"}, ...]
|
|
405
|
+
allLanes = config.lanes;
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
// New format with definitions
|
|
409
|
+
if (config.lanes.definitions) {
|
|
410
|
+
allLanes.push(...config.lanes.definitions);
|
|
411
|
+
}
|
|
412
|
+
// Legacy nested format: lanes: {engineering: [...], business: [...]}
|
|
413
|
+
if (config.lanes.engineering) {
|
|
414
|
+
allLanes.push(...config.lanes.engineering);
|
|
415
|
+
}
|
|
416
|
+
if (config.lanes.business) {
|
|
417
|
+
allLanes.push(...config.lanes.business);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
// Find matching lane (case-insensitive)
|
|
421
|
+
const matchingLane = allLanes.find((l) => l.name.toLowerCase().trim() === normalizedLane);
|
|
422
|
+
if (!matchingLane) {
|
|
423
|
+
return DEFAULT_LOCK_POLICY;
|
|
424
|
+
}
|
|
425
|
+
// Return lock_policy if specified and valid, otherwise default
|
|
426
|
+
const policy = matchingLane.lock_policy;
|
|
427
|
+
if (policy === 'all' || policy === 'active' || policy === 'none') {
|
|
428
|
+
return policy;
|
|
429
|
+
}
|
|
430
|
+
return DEFAULT_LOCK_POLICY;
|
|
431
|
+
}
|
|
432
|
+
catch {
|
|
433
|
+
// If config parsing fails, return default
|
|
434
|
+
return DEFAULT_LOCK_POLICY;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
345
437
|
/** WU-1197: Section heading marker for H2 headings */
|
|
346
438
|
const SECTION_HEADING_PREFIX = '## ';
|
|
347
439
|
/**
|
|
@@ -379,6 +471,38 @@ function extractInProgressSection(lines) {
|
|
|
379
471
|
const section = lines.slice(inProgressIdx + 1, endIdx).join(STRING_LITERALS.NEWLINE);
|
|
380
472
|
return { section, error: null };
|
|
381
473
|
}
|
|
474
|
+
/** WU-1324: Blocked section header patterns */
|
|
475
|
+
const BLOCKED_HEADERS = ['## blocked', '## ⛔ blocked'];
|
|
476
|
+
/**
|
|
477
|
+
* WU-1324: Check if a line matches a Blocked section header.
|
|
478
|
+
* @param {string} line - Line to check (will be trimmed and lowercased)
|
|
479
|
+
* @returns {boolean} True if line is a Blocked header
|
|
480
|
+
*/
|
|
481
|
+
function isBlockedHeader(line) {
|
|
482
|
+
const normalized = line.trim().toLowerCase();
|
|
483
|
+
return BLOCKED_HEADERS.some((header) => normalized === header || normalized.startsWith(header));
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* WU-1324: Extract Blocked section from status.md lines
|
|
487
|
+
* @returns Section content (may be empty if section doesn't exist)
|
|
488
|
+
*/
|
|
489
|
+
function extractBlockedSection(lines) {
|
|
490
|
+
const blockedIdx = lines.findIndex((l) => isBlockedHeader(l));
|
|
491
|
+
if (blockedIdx === -1) {
|
|
492
|
+
// Blocked section doesn't exist - return empty
|
|
493
|
+
return { section: '' };
|
|
494
|
+
}
|
|
495
|
+
// Find end of Blocked section (next ## heading or end of file)
|
|
496
|
+
let endIdx = lines.slice(blockedIdx + 1).findIndex((l) => l.startsWith(SECTION_HEADING_PREFIX));
|
|
497
|
+
if (endIdx === -1) {
|
|
498
|
+
endIdx = lines.length - blockedIdx - 1;
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
endIdx = blockedIdx + 1 + endIdx;
|
|
502
|
+
}
|
|
503
|
+
const section = lines.slice(blockedIdx + 1, endIdx).join(STRING_LITERALS.NEWLINE);
|
|
504
|
+
return { section };
|
|
505
|
+
}
|
|
382
506
|
/**
|
|
383
507
|
* WU-1197: Check if a WU belongs to the target lane
|
|
384
508
|
* @returns The WU ID if it matches the target lane, null otherwise
|
|
@@ -430,12 +554,37 @@ function collectInProgressWUsForLane(matches, wuid, projectRoot, targetLane) {
|
|
|
430
554
|
return inProgressWUs;
|
|
431
555
|
}
|
|
432
556
|
/**
|
|
433
|
-
*
|
|
557
|
+
* WU-1324: Extract WU IDs from a section's WU links and filter by target lane
|
|
558
|
+
* @param section - Section content from status.md
|
|
559
|
+
* @param wuid - WU ID being claimed (excluded from results)
|
|
560
|
+
* @param projectRoot - Project root path
|
|
561
|
+
* @param targetLane - Target lane name (normalized lowercase)
|
|
562
|
+
* @returns Array of WU IDs in the target lane
|
|
563
|
+
*/
|
|
564
|
+
function extractWUsFromSection(section, wuid, projectRoot, targetLane) {
|
|
565
|
+
if (!section || section.includes(NO_ITEMS_MARKER)) {
|
|
566
|
+
return [];
|
|
567
|
+
}
|
|
568
|
+
// Extract WU IDs from links like [WU-334 — Title](wu/WU-334.yaml)
|
|
569
|
+
WU_LINK_PATTERN.lastIndex = 0; // Reset global regex state
|
|
570
|
+
const matches = [...section.matchAll(WU_LINK_PATTERN)];
|
|
571
|
+
if (matches.length === 0) {
|
|
572
|
+
return [];
|
|
573
|
+
}
|
|
574
|
+
return collectInProgressWUsForLane(matches, wuid, projectRoot, targetLane);
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Check if a lane is free (WU count is below wip_limit)
|
|
434
578
|
*
|
|
435
579
|
* WU-1016: Now respects configurable wip_limit per lane from .lumenflow.config.yaml.
|
|
436
|
-
* Lane is considered "free" if current
|
|
580
|
+
* Lane is considered "free" if current WU count < wip_limit.
|
|
437
581
|
* Default wip_limit is 1 if not specified in config (backward compatible).
|
|
438
582
|
*
|
|
583
|
+
* WU-1324: Now respects lock_policy for WIP counting:
|
|
584
|
+
* - 'all' (default): Count in_progress + blocked WUs toward WIP limit
|
|
585
|
+
* - 'active': Count only in_progress WUs (blocked WUs release lane lock)
|
|
586
|
+
* - 'none': Disable WIP checking entirely (lane always free)
|
|
587
|
+
*
|
|
439
588
|
* @param {string} statusPath - Path to status.md
|
|
440
589
|
* @param {string} lane - Lane name (e.g., "Operations", "Intelligence")
|
|
441
590
|
* @param {string} wuid - WU ID being claimed (e.g., "WU-419")
|
|
@@ -444,40 +593,47 @@ function collectInProgressWUsForLane(matches, wuid, projectRoot, targetLane) {
|
|
|
444
593
|
*/
|
|
445
594
|
export function checkLaneFree(statusPath, lane, wuid, options = {}) {
|
|
446
595
|
try {
|
|
596
|
+
// WU-1016: Get WIP limit for this lane from config
|
|
597
|
+
const wipLimit = getWipLimitForLane(lane, { configPath: options.configPath });
|
|
598
|
+
// WU-1324: Get lock policy for this lane from config
|
|
599
|
+
const lockPolicy = getLockPolicyForLane(lane, { configPath: options.configPath });
|
|
600
|
+
// WU-1324: If policy is 'none', WIP checking is disabled - lane is always free
|
|
601
|
+
if (lockPolicy === 'none') {
|
|
602
|
+
return createEmptyLaneResult(wipLimit);
|
|
603
|
+
}
|
|
447
604
|
// Read status.md
|
|
448
605
|
if (!existsSync(statusPath)) {
|
|
449
606
|
return { free: false, occupiedBy: null, error: `status.md not found: ${statusPath}` };
|
|
450
607
|
}
|
|
451
608
|
const content = readFileSync(statusPath, { encoding: 'utf-8' });
|
|
452
609
|
const lines = content.split(/\r?\n/);
|
|
453
|
-
|
|
610
|
+
// Extract In Progress section
|
|
611
|
+
const { section: inProgressSection, error } = extractInProgressSection(lines);
|
|
454
612
|
if (error) {
|
|
455
613
|
return { free: false, occupiedBy: null, error };
|
|
456
614
|
}
|
|
457
|
-
// WU-1016: Get WIP limit for this lane from config
|
|
458
|
-
const wipLimit = getWipLimitForLane(lane, { configPath: options.configPath });
|
|
459
|
-
// Check for "No items" marker or no WU links
|
|
460
|
-
if (section.includes(NO_ITEMS_MARKER)) {
|
|
461
|
-
return createEmptyLaneResult(wipLimit);
|
|
462
|
-
}
|
|
463
|
-
// Extract WU IDs from links like [WU-334 — Title](wu/WU-334.yaml)
|
|
464
|
-
WU_LINK_PATTERN.lastIndex = 0; // Reset global regex state
|
|
465
|
-
const matches = [...section.matchAll(WU_LINK_PATTERN)];
|
|
466
|
-
if (matches.length === 0) {
|
|
467
|
-
return createEmptyLaneResult(wipLimit);
|
|
468
|
-
}
|
|
469
615
|
// Get project root from statusPath (docs/04-operations/tasks/status.md)
|
|
470
616
|
const projectRoot = path.dirname(path.dirname(path.dirname(path.dirname(statusPath))));
|
|
471
617
|
const targetLane = lane.toString().trim().toLowerCase();
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
618
|
+
// Collect in_progress WUs
|
|
619
|
+
const inProgressWUs = extractWUsFromSection(inProgressSection, wuid, projectRoot, targetLane);
|
|
620
|
+
// WU-1324: If policy is 'all', also count blocked WUs
|
|
621
|
+
let blockedWUs = [];
|
|
622
|
+
if (lockPolicy === 'all') {
|
|
623
|
+
const { section: blockedSection } = extractBlockedSection(lines);
|
|
624
|
+
blockedWUs = extractWUsFromSection(blockedSection, wuid, projectRoot, targetLane);
|
|
625
|
+
}
|
|
626
|
+
// WU-1324: Calculate total count based on policy
|
|
627
|
+
// - 'all': in_progress + blocked
|
|
628
|
+
// - 'active': in_progress only
|
|
629
|
+
const allCountedWUs = [...inProgressWUs, ...blockedWUs];
|
|
630
|
+
const currentCount = allCountedWUs.length;
|
|
475
631
|
const isFree = currentCount < wipLimit;
|
|
476
632
|
return {
|
|
477
633
|
free: isFree,
|
|
478
|
-
occupiedBy: isFree ? null :
|
|
634
|
+
occupiedBy: isFree ? null : allCountedWUs[0] || null,
|
|
479
635
|
error: null,
|
|
480
|
-
inProgressWUs,
|
|
636
|
+
inProgressWUs: allCountedWUs, // Include all counted WUs for visibility
|
|
481
637
|
wipLimit,
|
|
482
638
|
currentCount,
|
|
483
639
|
};
|
package/dist/lane-lock.d.ts
CHANGED
|
@@ -12,7 +12,13 @@
|
|
|
12
12
|
* Lock file location: .lumenflow/locks/<lane-kebab>.lock
|
|
13
13
|
* Lock file format: JSON with wuId, timestamp, agentSession, pid
|
|
14
14
|
*
|
|
15
|
+
* Lock policy support (WU-1323):
|
|
16
|
+
* - 'all' (default): Lock held through entire WU lifecycle
|
|
17
|
+
* - 'active': Lock released on block, re-acquired on unblock (CLI behavior)
|
|
18
|
+
* - 'none': No lock files created, WIP checking disabled for the lane
|
|
19
|
+
*
|
|
15
20
|
* @see WU-1603 - Race condition fix for wu:claim
|
|
21
|
+
* @see WU-1323 - Lock policy integration tests
|
|
16
22
|
*/
|
|
17
23
|
export interface LockMetadata {
|
|
18
24
|
wuId: string;
|
|
@@ -26,6 +32,8 @@ interface LockResult {
|
|
|
26
32
|
error: string | null;
|
|
27
33
|
existingLock: LockMetadata | null;
|
|
28
34
|
isStale: boolean;
|
|
35
|
+
/** WU-1325: True if lock acquisition was skipped due to lock_policy=none */
|
|
36
|
+
skipped?: boolean;
|
|
29
37
|
}
|
|
30
38
|
interface UnlockResult {
|
|
31
39
|
released: boolean;
|
package/dist/lane-lock.js
CHANGED
|
@@ -12,11 +12,19 @@
|
|
|
12
12
|
* Lock file location: .lumenflow/locks/<lane-kebab>.lock
|
|
13
13
|
* Lock file format: JSON with wuId, timestamp, agentSession, pid
|
|
14
14
|
*
|
|
15
|
+
* Lock policy support (WU-1323):
|
|
16
|
+
* - 'all' (default): Lock held through entire WU lifecycle
|
|
17
|
+
* - 'active': Lock released on block, re-acquired on unblock (CLI behavior)
|
|
18
|
+
* - 'none': No lock files created, WIP checking disabled for the lane
|
|
19
|
+
*
|
|
15
20
|
* @see WU-1603 - Race condition fix for wu:claim
|
|
21
|
+
* @see WU-1323 - Lock policy integration tests
|
|
16
22
|
*/
|
|
17
23
|
import { openSync, closeSync, writeFileSync, readFileSync, unlinkSync, existsSync, mkdirSync, } from 'node:fs';
|
|
18
24
|
import path from 'node:path';
|
|
19
25
|
import { toKebab, LUMENFLOW_PATHS, getProjectRoot } from './wu-constants.js';
|
|
26
|
+
// WU-1325: Import lock policy getter
|
|
27
|
+
import { getLockPolicyForLane } from './lane-checker.js';
|
|
20
28
|
/** Log prefix for lane-lock messages */
|
|
21
29
|
const LOG_PREFIX = '[lane-lock]';
|
|
22
30
|
/** Directory for lock files relative to project root */
|
|
@@ -169,6 +177,19 @@ export function readLockMetadata(lockPath) {
|
|
|
169
177
|
// eslint-disable-next-line sonarjs/cognitive-complexity -- WU-1808: Added zombie lock detection increases complexity but all paths are necessary
|
|
170
178
|
export function acquireLaneLock(lane, wuId, options = {}) {
|
|
171
179
|
const { agentSession = null, baseDir = null } = options;
|
|
180
|
+
// WU-1325: Check lock policy before acquiring
|
|
181
|
+
const lockPolicy = getLockPolicyForLane(lane);
|
|
182
|
+
if (lockPolicy === 'none') {
|
|
183
|
+
// eslint-disable-next-line no-console -- CLI tool status message
|
|
184
|
+
console.log(`${LOG_PREFIX} Skipping lock acquisition for "${lane}" (lock_policy=none)`);
|
|
185
|
+
return {
|
|
186
|
+
acquired: true,
|
|
187
|
+
error: null,
|
|
188
|
+
existingLock: null,
|
|
189
|
+
isStale: false,
|
|
190
|
+
skipped: true,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
172
193
|
try {
|
|
173
194
|
ensureLocksDir(baseDir);
|
|
174
195
|
const lockPath = getLockFilePath(lane, baseDir);
|
|
@@ -7,6 +7,30 @@
|
|
|
7
7
|
* @module lumenflow-config-schema
|
|
8
8
|
*/
|
|
9
9
|
import { z } from 'zod';
|
|
10
|
+
/**
|
|
11
|
+
* WU-1325: Lock policy for lane-level WIP enforcement
|
|
12
|
+
*
|
|
13
|
+
* Controls how lane locks behave:
|
|
14
|
+
* - 'all' (default): Lock acquired on claim, held through block, released on done
|
|
15
|
+
* - 'active': Lock acquired on claim, released on block, re-acquired on unblock
|
|
16
|
+
* - 'none': No lock files created, WIP checking disabled
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```yaml
|
|
20
|
+
* lanes:
|
|
21
|
+
* definitions:
|
|
22
|
+
* - name: 'Content: Documentation'
|
|
23
|
+
* wip_limit: 4
|
|
24
|
+
* lock_policy: 'none' # Docs don't need lock coordination
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export declare const LockPolicySchema: z.ZodDefault<z.ZodEnum<{
|
|
28
|
+
none: "none";
|
|
29
|
+
all: "all";
|
|
30
|
+
active: "active";
|
|
31
|
+
}>>;
|
|
32
|
+
/** WU-1325: TypeScript type for lock policy */
|
|
33
|
+
export type LockPolicy = z.infer<typeof LockPolicySchema>;
|
|
10
34
|
/**
|
|
11
35
|
* Event archival configuration (WU-1207)
|
|
12
36
|
*
|
|
@@ -36,6 +60,8 @@ export declare const DirectoriesSchema: z.ZodObject<{
|
|
|
36
60
|
skillsDir: z.ZodDefault<z.ZodString>;
|
|
37
61
|
agentsDir: z.ZodDefault<z.ZodString>;
|
|
38
62
|
plansDir: z.ZodDefault<z.ZodString>;
|
|
63
|
+
templatesDir: z.ZodDefault<z.ZodString>;
|
|
64
|
+
onboardingDir: z.ZodDefault<z.ZodString>;
|
|
39
65
|
}, z.core.$strip>;
|
|
40
66
|
/**
|
|
41
67
|
* Beacon paths configuration (.lumenflow directory structure)
|
|
@@ -56,6 +82,19 @@ export declare const BeaconPathsSchema: z.ZodObject<{
|
|
|
56
82
|
keepArchives: z.ZodDefault<z.ZodBoolean>;
|
|
57
83
|
}, z.core.$strip>>;
|
|
58
84
|
}, z.core.$strip>;
|
|
85
|
+
/**
|
|
86
|
+
* WU-1332: Push retry configuration for micro-worktree operations
|
|
87
|
+
*
|
|
88
|
+
* When non-fast-forward push errors occur (origin/main moved during operation),
|
|
89
|
+
* retry with exponential backoff. Uses p-retry for robust retry behavior.
|
|
90
|
+
*/
|
|
91
|
+
export declare const PushRetryConfigSchema: z.ZodObject<{
|
|
92
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
93
|
+
retries: z.ZodDefault<z.ZodNumber>;
|
|
94
|
+
min_delay_ms: z.ZodDefault<z.ZodNumber>;
|
|
95
|
+
max_delay_ms: z.ZodDefault<z.ZodNumber>;
|
|
96
|
+
jitter: z.ZodDefault<z.ZodBoolean>;
|
|
97
|
+
}, z.core.$strip>;
|
|
59
98
|
/**
|
|
60
99
|
* Git configuration
|
|
61
100
|
*/
|
|
@@ -72,6 +111,13 @@ export declare const GitConfigSchema: z.ZodObject<{
|
|
|
72
111
|
agentBranchPatterns: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
73
112
|
agentBranchPatternsOverride: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
74
113
|
disableAgentPatternRegistry: z.ZodDefault<z.ZodBoolean>;
|
|
114
|
+
push_retry: z.ZodDefault<z.ZodObject<{
|
|
115
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
116
|
+
retries: z.ZodDefault<z.ZodNumber>;
|
|
117
|
+
min_delay_ms: z.ZodDefault<z.ZodNumber>;
|
|
118
|
+
max_delay_ms: z.ZodDefault<z.ZodNumber>;
|
|
119
|
+
jitter: z.ZodDefault<z.ZodBoolean>;
|
|
120
|
+
}, z.core.$strip>>;
|
|
75
121
|
}, z.core.$strip>;
|
|
76
122
|
/**
|
|
77
123
|
* WU (Work Unit) configuration
|
|
@@ -311,6 +357,23 @@ export declare const TelemetryConfigSchema: z.ZodObject<{
|
|
|
311
357
|
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
312
358
|
}, z.core.$strip>>;
|
|
313
359
|
}, z.core.$strip>;
|
|
360
|
+
/**
|
|
361
|
+
* WU-1322: Lane definition schema for .lumenflow.config.yaml
|
|
362
|
+
*
|
|
363
|
+
* Extends the existing lane configuration with lock_policy field.
|
|
364
|
+
* Compatible with WU-1016 (wip_limit) and WU-1187 (wip_justification).
|
|
365
|
+
*/
|
|
366
|
+
export declare const LaneDefinitionSchema: z.ZodObject<{
|
|
367
|
+
name: z.ZodString;
|
|
368
|
+
wip_limit: z.ZodOptional<z.ZodNumber>;
|
|
369
|
+
wip_justification: z.ZodOptional<z.ZodString>;
|
|
370
|
+
lock_policy: z.ZodDefault<z.ZodDefault<z.ZodEnum<{
|
|
371
|
+
none: "none";
|
|
372
|
+
all: "all";
|
|
373
|
+
active: "active";
|
|
374
|
+
}>>>;
|
|
375
|
+
code_paths: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
376
|
+
}, z.core.$strip>;
|
|
314
377
|
/**
|
|
315
378
|
* Complete LumenFlow configuration schema
|
|
316
379
|
*/
|
|
@@ -332,6 +395,8 @@ export declare const LumenFlowConfigSchema: z.ZodObject<{
|
|
|
332
395
|
skillsDir: z.ZodDefault<z.ZodString>;
|
|
333
396
|
agentsDir: z.ZodDefault<z.ZodString>;
|
|
334
397
|
plansDir: z.ZodDefault<z.ZodString>;
|
|
398
|
+
templatesDir: z.ZodDefault<z.ZodString>;
|
|
399
|
+
onboardingDir: z.ZodDefault<z.ZodString>;
|
|
335
400
|
}, z.core.$strip>>;
|
|
336
401
|
beacon: z.ZodDefault<z.ZodObject<{
|
|
337
402
|
base: z.ZodDefault<z.ZodString>;
|
|
@@ -362,6 +427,13 @@ export declare const LumenFlowConfigSchema: z.ZodObject<{
|
|
|
362
427
|
agentBranchPatterns: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
363
428
|
agentBranchPatternsOverride: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
364
429
|
disableAgentPatternRegistry: z.ZodDefault<z.ZodBoolean>;
|
|
430
|
+
push_retry: z.ZodDefault<z.ZodObject<{
|
|
431
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
432
|
+
retries: z.ZodDefault<z.ZodNumber>;
|
|
433
|
+
min_delay_ms: z.ZodDefault<z.ZodNumber>;
|
|
434
|
+
max_delay_ms: z.ZodDefault<z.ZodNumber>;
|
|
435
|
+
jitter: z.ZodDefault<z.ZodBoolean>;
|
|
436
|
+
}, z.core.$strip>>;
|
|
365
437
|
}, z.core.$strip>>;
|
|
366
438
|
wu: z.ZodDefault<z.ZodObject<{
|
|
367
439
|
idPattern: z.ZodDefault<z.ZodString>;
|
|
@@ -505,6 +577,7 @@ export declare const LumenFlowConfigSchema: z.ZodObject<{
|
|
|
505
577
|
*/
|
|
506
578
|
export type Directories = z.infer<typeof DirectoriesSchema>;
|
|
507
579
|
export type BeaconPaths = z.infer<typeof BeaconPathsSchema>;
|
|
580
|
+
export type PushRetryConfig = z.infer<typeof PushRetryConfigSchema>;
|
|
508
581
|
export type GitConfig = z.infer<typeof GitConfigSchema>;
|
|
509
582
|
export type WuConfig = z.infer<typeof WuConfigSchema>;
|
|
510
583
|
export type GatesConfig = z.infer<typeof GatesConfigSchema>;
|
|
@@ -524,6 +597,7 @@ export type ValidationMode = z.infer<typeof ValidationModeSchema>;
|
|
|
524
597
|
export type MethodologyTelemetryConfig = z.infer<typeof MethodologyTelemetryConfigSchema>;
|
|
525
598
|
export type TelemetryConfig = z.infer<typeof TelemetryConfigSchema>;
|
|
526
599
|
export type LumenFlowConfig = z.infer<typeof LumenFlowConfigSchema>;
|
|
600
|
+
export type LaneDefinition = z.infer<typeof LaneDefinitionSchema>;
|
|
527
601
|
export type { MethodologyConfig, MethodologyOverrides } from './resolve-policy.js';
|
|
528
602
|
/**
|
|
529
603
|
* Validate configuration data
|
|
@@ -549,6 +623,8 @@ export declare function validateConfig(data: unknown): z.ZodSafeParseResult<{
|
|
|
549
623
|
skillsDir: string;
|
|
550
624
|
agentsDir: string;
|
|
551
625
|
plansDir: string;
|
|
626
|
+
templatesDir: string;
|
|
627
|
+
onboardingDir: string;
|
|
552
628
|
};
|
|
553
629
|
beacon: {
|
|
554
630
|
base: string;
|
|
@@ -578,6 +654,13 @@ export declare function validateConfig(data: unknown): z.ZodSafeParseResult<{
|
|
|
578
654
|
requireRemote: boolean;
|
|
579
655
|
agentBranchPatterns: string[];
|
|
580
656
|
disableAgentPatternRegistry: boolean;
|
|
657
|
+
push_retry: {
|
|
658
|
+
enabled: boolean;
|
|
659
|
+
retries: number;
|
|
660
|
+
min_delay_ms: number;
|
|
661
|
+
max_delay_ms: number;
|
|
662
|
+
jitter: boolean;
|
|
663
|
+
};
|
|
581
664
|
agentBranchPatternsOverride?: string[];
|
|
582
665
|
};
|
|
583
666
|
wu: {
|