@lumenflow/core 2.3.2 → 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 +21 -2
- package/dist/arg-parser.js +74 -1
- package/dist/context/wu-state-reader.js +3 -2
- package/dist/lane-checker.d.ts +29 -2
- package/dist/lane-checker.js +176 -20
- package/dist/lane-inference.js +8 -2
- package/dist/lane-lock.d.ts +8 -0
- package/dist/lane-lock.js +21 -0
- package/dist/lumenflow-config-schema.d.ts +89 -0
- package/dist/lumenflow-config-schema.js +133 -0
- package/dist/lumenflow-config.d.ts +5 -1
- package/dist/lumenflow-config.js +12 -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 +30 -0
- package/dist/wu-paths.js +15 -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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type OptionValues } from 'commander';
|
|
1
2
|
/**
|
|
2
3
|
* Predefined option configurations for WU management scripts.
|
|
3
4
|
* Each option has: name, flags, description, and optional default.
|
|
@@ -19,6 +20,10 @@ interface WUOption {
|
|
|
19
20
|
default?: string | boolean | string[];
|
|
20
21
|
isNegated?: boolean;
|
|
21
22
|
isRepeatable?: boolean;
|
|
23
|
+
/** Type hint for option parsing (e.g., 'boolean' for flags, 'string' for values) */
|
|
24
|
+
type?: 'boolean' | 'string';
|
|
25
|
+
/** Whether this option is required (used for validation hints) */
|
|
26
|
+
required?: boolean;
|
|
22
27
|
}
|
|
23
28
|
export declare const WU_OPTIONS: Record<string, WUOption>;
|
|
24
29
|
/**
|
|
@@ -27,6 +32,13 @@ export declare const WU_OPTIONS: Record<string, WUOption>;
|
|
|
27
32
|
* These options control how wu:create handles external plan storage.
|
|
28
33
|
*/
|
|
29
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[];
|
|
30
42
|
/**
|
|
31
43
|
* Create a commander-based CLI parser for a WU script.
|
|
32
44
|
*
|
|
@@ -49,7 +61,14 @@ export declare const WU_CREATE_OPTIONS: Record<string, WUOption>;
|
|
|
49
61
|
* console.log(opts.id); // 'WU-123'
|
|
50
62
|
* console.log(opts.branchOnly); // true
|
|
51
63
|
*/
|
|
52
|
-
export declare function createWUParser(config:
|
|
64
|
+
export declare function createWUParser(config: {
|
|
65
|
+
name: string;
|
|
66
|
+
description: string;
|
|
67
|
+
options?: WUOption[];
|
|
68
|
+
required?: string[];
|
|
69
|
+
allowPositionalId?: boolean;
|
|
70
|
+
version?: string;
|
|
71
|
+
}): OptionValues;
|
|
53
72
|
/**
|
|
54
73
|
* Backward-compatible unified argument parser for WU management scripts.
|
|
55
74
|
* Uses commander internally but maintains the same return format.
|
|
@@ -65,5 +84,5 @@ export declare function createWUParser(config: any): import("commander").OptionV
|
|
|
65
84
|
* console.log(args.id); // 'WU-123'
|
|
66
85
|
* console.log(args.branchOnly); // true
|
|
67
86
|
*/
|
|
68
|
-
export declare function parseWUArgs(argv:
|
|
87
|
+
export declare function parseWUArgs(argv: string[]): OptionValues;
|
|
69
88
|
export {};
|
package/dist/arg-parser.js
CHANGED
|
@@ -323,12 +323,26 @@ export const WU_OPTIONS = {
|
|
|
323
323
|
description: 'Code paths (repeatable)',
|
|
324
324
|
isRepeatable: true,
|
|
325
325
|
},
|
|
326
|
+
// WU-1300: Alias for --code-paths (singular form for convenience)
|
|
327
|
+
codePath: {
|
|
328
|
+
name: 'codePath',
|
|
329
|
+
flags: '--code-path <path>',
|
|
330
|
+
description: 'Alias for --code-paths (repeatable)',
|
|
331
|
+
isRepeatable: true,
|
|
332
|
+
},
|
|
326
333
|
testPathsManual: {
|
|
327
334
|
name: 'testPathsManual',
|
|
328
335
|
flags: '--test-paths-manual <tests>',
|
|
329
336
|
description: 'Manual test descriptions (repeatable)',
|
|
330
337
|
isRepeatable: true,
|
|
331
338
|
},
|
|
339
|
+
// WU-1300: Alias for --test-paths-manual (shorter form for convenience)
|
|
340
|
+
manualTest: {
|
|
341
|
+
name: 'manualTest',
|
|
342
|
+
flags: '--manual-test <test>',
|
|
343
|
+
description: 'Alias for --test-paths-manual (repeatable)',
|
|
344
|
+
isRepeatable: true,
|
|
345
|
+
},
|
|
332
346
|
testPathsUnit: {
|
|
333
347
|
name: 'testPathsUnit',
|
|
334
348
|
flags: '--test-paths-unit <paths>',
|
|
@@ -424,6 +438,14 @@ export const WU_OPTIONS = {
|
|
|
424
438
|
flags: '--skip-setup',
|
|
425
439
|
description: 'Skip automatic pnpm install in worktree after creation (faster claims when deps already built)',
|
|
426
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
|
+
},
|
|
427
449
|
};
|
|
428
450
|
/**
|
|
429
451
|
* WU-1062: Additional options for wu:create command
|
|
@@ -444,8 +466,10 @@ export const WU_CREATE_OPTIONS = {
|
|
|
444
466
|
/**
|
|
445
467
|
* Negated options that commander handles specially.
|
|
446
468
|
* --no-foo creates opts.foo = false. We convert to noFoo = true.
|
|
469
|
+
*
|
|
470
|
+
* WU-1329: Export for testing purposes.
|
|
447
471
|
*/
|
|
448
|
-
const NEGATED_OPTIONS = ['auto', 'remove', 'merge', 'autoRebase', 'push'];
|
|
472
|
+
export const NEGATED_OPTIONS = ['auto', 'remove', 'merge', 'autoRebase', 'push', 'strict'];
|
|
449
473
|
/**
|
|
450
474
|
* Post-process commander opts to handle negated boolean options.
|
|
451
475
|
* Commander's --no-* flags create opts.foo = false.
|
|
@@ -562,8 +586,57 @@ export function createWUParser(config) {
|
|
|
562
586
|
if (allowPositionalId && program.args.length > 0 && !opts.id) {
|
|
563
587
|
opts.id = program.args[0];
|
|
564
588
|
}
|
|
589
|
+
// WU-1300: Merge CLI aliases into their canonical options
|
|
590
|
+
opts = mergeAliasOptions(opts);
|
|
565
591
|
return opts;
|
|
566
592
|
}
|
|
593
|
+
/**
|
|
594
|
+
* WU-1300: Option alias mappings (alias -> canonical)
|
|
595
|
+
* These allow users to use shorter/alternative flag names.
|
|
596
|
+
*/
|
|
597
|
+
const OPTION_ALIASES = {
|
|
598
|
+
codePath: 'codePaths',
|
|
599
|
+
manualTest: 'testPathsManual',
|
|
600
|
+
};
|
|
601
|
+
/**
|
|
602
|
+
* WU-1300: Merge alias options into their canonical counterparts.
|
|
603
|
+
* Supports both singular aliases (--code-path -> --code-paths)
|
|
604
|
+
* and alternative names (--manual-test -> --test-paths-manual).
|
|
605
|
+
*
|
|
606
|
+
* For repeatable options, values are concatenated.
|
|
607
|
+
* For single-value options, alias value is used if canonical is not set.
|
|
608
|
+
*
|
|
609
|
+
* @param {object} opts - Parsed options from Commander
|
|
610
|
+
* @returns {object} Options with aliases merged into canonical names
|
|
611
|
+
*/
|
|
612
|
+
function mergeAliasOptions(opts) {
|
|
613
|
+
const result = { ...opts };
|
|
614
|
+
for (const [alias, canonical] of Object.entries(OPTION_ALIASES)) {
|
|
615
|
+
const aliasValue = result[alias];
|
|
616
|
+
const canonicalValue = result[canonical];
|
|
617
|
+
if (aliasValue !== undefined && aliasValue !== null) {
|
|
618
|
+
// For arrays (repeatable options), concatenate
|
|
619
|
+
if (Array.isArray(aliasValue)) {
|
|
620
|
+
const existingArray = Array.isArray(canonicalValue) ? canonicalValue : [];
|
|
621
|
+
result[canonical] = [...existingArray, ...aliasValue];
|
|
622
|
+
}
|
|
623
|
+
else if (canonicalValue === undefined || canonicalValue === null) {
|
|
624
|
+
// For single values, only use alias if canonical not set
|
|
625
|
+
result[canonical] = aliasValue;
|
|
626
|
+
}
|
|
627
|
+
// Remove the alias from results (clean output)
|
|
628
|
+
// Build new result without the alias key to avoid dynamic delete
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
// Remove alias keys from final result
|
|
632
|
+
const finalResult = {};
|
|
633
|
+
for (const [key, value] of Object.entries(result)) {
|
|
634
|
+
if (!(key in OPTION_ALIASES)) {
|
|
635
|
+
finalResult[key] = value;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
return finalResult;
|
|
639
|
+
}
|
|
567
640
|
/**
|
|
568
641
|
* Backward-compatible unified argument parser for WU management scripts.
|
|
569
642
|
* Uses commander internally but maintains the same return format.
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { existsSync, readFileSync } from 'node:fs';
|
|
12
12
|
import { join } from 'node:path';
|
|
13
13
|
import { parse as parseYaml } from 'yaml';
|
|
14
|
-
import {
|
|
14
|
+
import { WU_PATHS } from '../wu-paths.js';
|
|
15
15
|
/**
|
|
16
16
|
* Normalize WU ID to uppercase format.
|
|
17
17
|
*/
|
|
@@ -24,10 +24,11 @@ function normalizeWuId(id) {
|
|
|
24
24
|
}
|
|
25
25
|
/**
|
|
26
26
|
* Build the path to WU YAML file.
|
|
27
|
+
* WU-1301: Uses config-based paths instead of hardcoded DIRECTORIES.
|
|
27
28
|
*/
|
|
28
29
|
function getWuYamlPath(wuId, repoRoot) {
|
|
29
30
|
const normalizedId = normalizeWuId(wuId);
|
|
30
|
-
return join(repoRoot,
|
|
31
|
+
return join(repoRoot, WU_PATHS.WU(normalizedId));
|
|
31
32
|
}
|
|
32
33
|
/**
|
|
33
34
|
* Read WU state from YAML and detect inconsistencies.
|
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-inference.js
CHANGED
|
@@ -31,7 +31,13 @@ function loadConfig(configPath = null) {
|
|
|
31
31
|
configPath = path.join(projectRoot, '.lumenflow.lane-inference.yaml');
|
|
32
32
|
}
|
|
33
33
|
if (!existsSync(configPath)) {
|
|
34
|
-
throw createError(ErrorCodes.FILE_NOT_FOUND, `Lane inference config not found: ${configPath}\n\
|
|
34
|
+
throw createError(ErrorCodes.FILE_NOT_FOUND, `Lane inference config not found: ${configPath}\n\n` +
|
|
35
|
+
`This file defines the lane taxonomy for sub-lane validation.\n\n` +
|
|
36
|
+
`To fix this:\n` +
|
|
37
|
+
` 1. Generate a lane taxonomy from your codebase:\n` +
|
|
38
|
+
` pnpm lane:suggest --output .lumenflow.lane-inference.yaml\n\n` +
|
|
39
|
+
` 2. Or copy from an example project and customize.\n\n` +
|
|
40
|
+
`See: LUMENFLOW.md "Setup Notes" section for details.`, { path: configPath });
|
|
35
41
|
}
|
|
36
42
|
try {
|
|
37
43
|
const configContent = readFileSync(configPath, { encoding: 'utf-8' });
|
|
@@ -167,7 +173,7 @@ export function getAllSubLanes(configPath = null) {
|
|
|
167
173
|
subLanes.push(`${parentLane}: ${subLane}`);
|
|
168
174
|
}
|
|
169
175
|
}
|
|
170
|
-
return subLanes.sort();
|
|
176
|
+
return subLanes.sort((a, b) => a.localeCompare(b));
|
|
171
177
|
}
|
|
172
178
|
/**
|
|
173
179
|
* Get valid sub-lanes for a specific parent lane
|
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);
|