@lumenflow/initiatives 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.
@@ -66,6 +66,13 @@ export declare const INIT_COMMIT_FORMATS: {
66
66
  * @returns Commit message
67
67
  */
68
68
  LINK_WU: (wuId: string, initId: string) => string;
69
+ /**
70
+ * Unlink WU from initiative commit message (WU-1328)
71
+ * @param wuId - WU ID
72
+ * @param initId - Initiative ID
73
+ * @returns Commit message
74
+ */
75
+ UNLINK_WU: (wuId: string, initId: string) => string;
69
76
  /**
70
77
  * Edit initiative commit message (WU-1451)
71
78
  * @param id - Initiative ID
@@ -81,6 +88,7 @@ export declare const INIT_LOG_PREFIX: {
81
88
  LIST: string;
82
89
  STATUS: string;
83
90
  ADD_WU: string;
91
+ REMOVE_WU: string;
84
92
  EDIT: string;
85
93
  };
86
94
  /**
@@ -69,6 +69,13 @@ export const INIT_COMMIT_FORMATS = {
69
69
  * @returns Commit message
70
70
  */
71
71
  LINK_WU: (wuId, initId) => `docs: link ${wuId.toLowerCase()} to ${initId.toLowerCase()}`,
72
+ /**
73
+ * Unlink WU from initiative commit message (WU-1328)
74
+ * @param wuId - WU ID
75
+ * @param initId - Initiative ID
76
+ * @returns Commit message
77
+ */
78
+ UNLINK_WU: (wuId, initId) => `docs: unlink ${wuId.toLowerCase()} from ${initId.toLowerCase()}`,
72
79
  /**
73
80
  * Edit initiative commit message (WU-1451)
74
81
  * @param id - Initiative ID
@@ -84,6 +91,7 @@ export const INIT_LOG_PREFIX = {
84
91
  LIST: '[initiative:list]',
85
92
  STATUS: '[initiative:status]',
86
93
  ADD_WU: '[initiative:add-wu]',
94
+ REMOVE_WU: '[initiative:remove-wu]', // WU-1328: Remove WU from initiative
87
95
  EDIT: '[initiative:edit]', // WU-1451: Initiative edit operation
88
96
  };
89
97
  /**
@@ -407,4 +407,71 @@ export declare function formatTaskInvocationWithEmbeddedSpawn(wu: WUEntry): stri
407
407
  * @returns {string} Formatted output with embedded Task invocations
408
408
  */
409
409
  export declare function formatExecutionPlanWithEmbeddedSpawns(plan: ExecutionPlan): string;
410
+ /**
411
+ * WU-1326: Lock policy type for lane configuration.
412
+ *
413
+ * - 'all' (default): Blocked WUs hold lane lock (current behavior)
414
+ * - 'active': Blocked WUs do NOT hold lane lock (only in_progress holds)
415
+ * - 'none': No WIP checking at all (unlimited parallel WUs in lane)
416
+ */
417
+ export type LockPolicy = 'all' | 'active' | 'none';
418
+ /**
419
+ * WU-1326: Lane configuration with lock_policy.
420
+ */
421
+ export interface LaneConfig {
422
+ lock_policy?: LockPolicy;
423
+ wip_limit?: number;
424
+ }
425
+ /**
426
+ * WU-1326: Options for lock_policy-aware execution plan building.
427
+ */
428
+ export interface LockPolicyOptions {
429
+ laneConfigs?: Record<string, LaneConfig>;
430
+ }
431
+ /**
432
+ * WU-1326: Lane availability result for policy-aware status display.
433
+ */
434
+ export interface LaneAvailabilityResult {
435
+ available: boolean;
436
+ policy: LockPolicy;
437
+ occupiedBy?: string;
438
+ blockedCount: number;
439
+ inProgressCount: number;
440
+ }
441
+ /**
442
+ * WU-1326: Get lock_policy for a lane from configuration.
443
+ *
444
+ * Returns the lock_policy from config if specified, otherwise defaults to 'all'
445
+ * for backward compatibility.
446
+ *
447
+ * @param {string} lane - Lane name (e.g., 'Framework: Core')
448
+ * @param {Record<string, LaneConfig> | undefined} laneConfigs - Lane configurations
449
+ * @returns {LockPolicy} The lock_policy for the lane ('all' | 'active' | 'none')
450
+ */
451
+ export declare function getLockPolicyForLane(lane: string, laneConfigs?: Record<string, LaneConfig>): LockPolicy;
452
+ /**
453
+ * WU-1326: Build execution plan respecting lock_policy per lane.
454
+ *
455
+ * This is an enhanced version of buildExecutionPlan that respects lock_policy
456
+ * when determining lane occupancy for wave building.
457
+ *
458
+ * When policy=active, blocked WUs do NOT prevent ready WUs in the same lane
459
+ * from being scheduled in the same wave.
460
+ *
461
+ * @param {Array<{id: string, doc: object}>} wus - WUs to plan
462
+ * @param {LockPolicyOptions} options - Lock policy options including laneConfigs
463
+ * @returns {ExecutionPlan} Execution plan with waves, skipped, and deferred WUs
464
+ */
465
+ export declare function buildExecutionPlanWithLockPolicy(wus: WUEntry[], options?: LockPolicyOptions): ExecutionPlan;
466
+ /**
467
+ * WU-1326: Get lane availability respecting lock_policy.
468
+ *
469
+ * Returns availability status for each lane based on current WU states
470
+ * and configured lock_policy.
471
+ *
472
+ * @param {Array<{id: string, doc: object}>} wus - WUs to check
473
+ * @param {LockPolicyOptions} options - Lock policy options
474
+ * @returns {Record<string, LaneAvailabilityResult>} Lane availability map
475
+ */
476
+ export declare function getLaneAvailability(wus: WUEntry[], options?: LockPolicyOptions): Record<string, LaneAvailabilityResult>;
410
477
  export { LOG_PREFIX };
@@ -103,6 +103,11 @@ export const CHECKPOINT_AUTO_THRESHOLDS = {
103
103
  * Using 'queued' makes it clear the WU is ready to be spawned but not yet running.
104
104
  */
105
105
  const MANIFEST_WU_STATUS = 'queued';
106
+ /**
107
+ * Default reason string for deferred WUs when no specific reason is provided.
108
+ * Extracted to constant to avoid sonarjs/no-duplicate-string lint warnings.
109
+ */
110
+ const DEFAULT_DEFERRED_REASON = 'waiting for dependencies';
106
111
  /**
107
112
  * WU-1200: Get the status string used in wave manifests for WUs.
108
113
  *
@@ -348,7 +353,7 @@ export function buildExecutionPlan(wus) {
348
353
  deferred.push({
349
354
  id: wu.id,
350
355
  blockedBy: Array.from(blockerSet),
351
- reason: reasonSet.size > 0 ? Array.from(reasonSet).join('; ') : 'waiting for dependencies',
356
+ reason: reasonSet.size > 0 ? Array.from(reasonSet).join('; ') : DEFAULT_DEFERRED_REASON,
352
357
  });
353
358
  }
354
359
  }
@@ -533,7 +538,7 @@ export async function buildExecutionPlanAsync(wus) {
533
538
  deferred.push({
534
539
  id: wu.id,
535
540
  blockedBy: Array.from(blockerSet),
536
- reason: reasonSet.size > 0 ? Array.from(reasonSet).join('; ') : 'waiting for dependencies',
541
+ reason: reasonSet.size > 0 ? Array.from(reasonSet).join('; ') : DEFAULT_DEFERRED_REASON,
537
542
  });
538
543
  }
539
544
  }
@@ -1413,4 +1418,374 @@ export function formatExecutionPlanWithEmbeddedSpawns(plan) {
1413
1418
  }
1414
1419
  return lines.join(STRING_LITERALS.NEWLINE);
1415
1420
  }
1421
+ /**
1422
+ * WU-1326: Get lock_policy for a lane from configuration.
1423
+ *
1424
+ * Returns the lock_policy from config if specified, otherwise defaults to 'all'
1425
+ * for backward compatibility.
1426
+ *
1427
+ * @param {string} lane - Lane name (e.g., 'Framework: Core')
1428
+ * @param {Record<string, LaneConfig> | undefined} laneConfigs - Lane configurations
1429
+ * @returns {LockPolicy} The lock_policy for the lane ('all' | 'active' | 'none')
1430
+ */
1431
+ export function getLockPolicyForLane(lane, laneConfigs) {
1432
+ if (!laneConfigs) {
1433
+ return 'all'; // Default for backward compatibility
1434
+ }
1435
+ const config = laneConfigs[lane];
1436
+ if (!config || !config.lock_policy) {
1437
+ return 'all'; // Default for unspecified lanes
1438
+ }
1439
+ return config.lock_policy;
1440
+ }
1441
+ /**
1442
+ * WU-1326: Check if a WU status holds the lane lock based on lock_policy.
1443
+ *
1444
+ * - policy=all: both 'in_progress' and 'blocked' hold lane lock
1445
+ * - policy=active: only 'in_progress' holds lane lock
1446
+ * - policy=none: nothing holds lane lock (no WIP checking)
1447
+ *
1448
+ * @param {string} status - WU status
1449
+ * @param {LockPolicy} policy - Lane lock policy
1450
+ * @returns {boolean} True if status holds lane lock
1451
+ */
1452
+ function _statusHoldsLaneLock(status, policy) {
1453
+ if (policy === 'none') {
1454
+ return false; // No WIP checking
1455
+ }
1456
+ if (policy === 'active') {
1457
+ // Only in_progress holds lane lock
1458
+ return status === WU_STATUS.IN_PROGRESS;
1459
+ }
1460
+ // policy === 'all' (default) - both in_progress and blocked hold lane
1461
+ return status === WU_STATUS.IN_PROGRESS || status === WU_STATUS.BLOCKED;
1462
+ }
1463
+ /**
1464
+ * WU-1326: Build execution plan respecting lock_policy per lane.
1465
+ *
1466
+ * This is an enhanced version of buildExecutionPlan that respects lock_policy
1467
+ * when determining lane occupancy for wave building.
1468
+ *
1469
+ * When policy=active, blocked WUs do NOT prevent ready WUs in the same lane
1470
+ * from being scheduled in the same wave.
1471
+ *
1472
+ * @param {Array<{id: string, doc: object}>} wus - WUs to plan
1473
+ * @param {LockPolicyOptions} options - Lock policy options including laneConfigs
1474
+ * @returns {ExecutionPlan} Execution plan with waves, skipped, and deferred WUs
1475
+ */
1476
+ // eslint-disable-next-line sonarjs/cognitive-complexity -- wave-building logic inherently complex
1477
+ export function buildExecutionPlanWithLockPolicy(wus, options = {}) {
1478
+ const { laneConfigs = {} } = options;
1479
+ // WU-2430: Enhanced categorisation of WUs
1480
+ const skipped = []; // IDs of done WUs (backwards compat)
1481
+ const skippedWithReasons = []; // WU-2430: Non-ready WUs with reasons
1482
+ const deferred = []; // WU-2430: Ready WUs waiting on external blockers
1483
+ const doneStatuses = new Set([WU_STATUS.DONE, WU_STATUS.COMPLETED]);
1484
+ // Categorise WUs by status
1485
+ for (const wu of wus) {
1486
+ const status = wu.doc.status ?? 'unknown';
1487
+ if (doneStatuses.has(status)) {
1488
+ skipped.push(wu.id);
1489
+ }
1490
+ else if (status !== WU_STATUS.READY) {
1491
+ skippedWithReasons.push({ id: wu.id, reason: `status: ${status}` });
1492
+ }
1493
+ }
1494
+ // WU-2430: Only ready WUs are candidates for execution
1495
+ const readyWUs = wus.filter((wu) => wu.doc.status === WU_STATUS.READY);
1496
+ if (readyWUs.length === 0) {
1497
+ return { waves: [], skipped, skippedWithReasons, deferred };
1498
+ }
1499
+ // Build a map for quick lookup
1500
+ const wuMap = new Map(readyWUs.map((wu) => [wu.id, wu]));
1501
+ const wuIds = new Set(wuMap.keys());
1502
+ const allWuMap = new Map(wus.map((wu) => [wu.id, wu]));
1503
+ const allWuIds = new Set(allWuMap.keys());
1504
+ // Build dependency graph for validation (check cycles)
1505
+ const graph = buildDependencyGraph();
1506
+ const { cycles } = validateGraph(graph);
1507
+ // Filter cycles to only those involving our WUs
1508
+ const relevantCycles = cycles.filter((cycle) => cycle.some((id) => wuIds.has(id)));
1509
+ if (relevantCycles.length > 0) {
1510
+ const cycleStr = relevantCycles.map((c) => c.join(' → ')).join('; ');
1511
+ throw createError(ErrorCodes.VALIDATION_ERROR, `Circular dependencies detected: ${cycleStr}`, {
1512
+ cycles: relevantCycles,
1513
+ });
1514
+ }
1515
+ // WU-2430: Check for external blockers without stamps
1516
+ const deferredIds = new Set();
1517
+ const deferredReasons = new Map();
1518
+ const deferredBlockers = new Map();
1519
+ const addDeferredEntry = (wuId, blockers, reason) => {
1520
+ deferredIds.add(wuId);
1521
+ let reasonSet = deferredReasons.get(wuId);
1522
+ let blockerSet = deferredBlockers.get(wuId);
1523
+ if (!reasonSet) {
1524
+ reasonSet = new Set();
1525
+ deferredReasons.set(wuId, reasonSet);
1526
+ }
1527
+ if (!blockerSet) {
1528
+ blockerSet = new Set();
1529
+ deferredBlockers.set(wuId, blockerSet);
1530
+ }
1531
+ for (const blockerId of blockers) {
1532
+ blockerSet.add(blockerId);
1533
+ }
1534
+ reasonSet.add(reason);
1535
+ };
1536
+ for (const wu of readyWUs) {
1537
+ const blockers = getAllDependencies(wu.doc);
1538
+ const externalBlockers = blockers.filter((blockerId) => !allWuIds.has(blockerId));
1539
+ const internalBlockers = blockers.filter((blockerId) => allWuIds.has(blockerId));
1540
+ if (externalBlockers.length > 0) {
1541
+ const unstampedBlockers = externalBlockers.filter((blockerId) => !hasStamp(blockerId));
1542
+ if (unstampedBlockers.length > 0) {
1543
+ addDeferredEntry(wu.id, unstampedBlockers, `waiting for external: ${unstampedBlockers.join(', ')}`);
1544
+ }
1545
+ }
1546
+ if (internalBlockers.length > 0) {
1547
+ const nonReadyInternal = internalBlockers.filter((blockerId) => {
1548
+ const blocker = allWuMap.get(blockerId);
1549
+ const status = blocker?.doc?.status ?? 'unknown';
1550
+ if (status === WU_STATUS.READY) {
1551
+ return false;
1552
+ }
1553
+ return !doneStatuses.has(status);
1554
+ });
1555
+ if (nonReadyInternal.length > 0) {
1556
+ const details = nonReadyInternal.map((blockerId) => {
1557
+ const status = allWuMap.get(blockerId)?.doc?.status ?? 'unknown';
1558
+ return `${blockerId} (status: ${status})`;
1559
+ });
1560
+ addDeferredEntry(wu.id, nonReadyInternal, `waiting for internal: ${details.join(', ')}`);
1561
+ }
1562
+ }
1563
+ }
1564
+ let hasNewDeferral = true;
1565
+ while (hasNewDeferral) {
1566
+ hasNewDeferral = false;
1567
+ for (const wu of readyWUs) {
1568
+ if (deferredIds.has(wu.id)) {
1569
+ continue;
1570
+ }
1571
+ const blockers = getAllDependencies(wu.doc);
1572
+ const deferredInternal = blockers.filter((blockerId) => allWuIds.has(blockerId) && deferredIds.has(blockerId));
1573
+ if (deferredInternal.length > 0) {
1574
+ const details = deferredInternal.map((blockerId) => {
1575
+ const status = allWuMap.get(blockerId)?.doc?.status ?? 'unknown';
1576
+ return `${blockerId} (status: ${status})`;
1577
+ });
1578
+ addDeferredEntry(wu.id, deferredInternal, `waiting for internal: ${details.join(', ')}`);
1579
+ hasNewDeferral = true;
1580
+ }
1581
+ }
1582
+ }
1583
+ for (const wu of readyWUs) {
1584
+ if (deferredIds.has(wu.id)) {
1585
+ const blockerSet = deferredBlockers.get(wu.id) || new Set();
1586
+ const reasonSet = deferredReasons.get(wu.id) || new Set();
1587
+ deferred.push({
1588
+ id: wu.id,
1589
+ blockedBy: Array.from(blockerSet),
1590
+ reason: reasonSet.size > 0 ? Array.from(reasonSet).join('; ') : DEFAULT_DEFERRED_REASON,
1591
+ });
1592
+ }
1593
+ }
1594
+ // Remove deferred WUs from candidates
1595
+ const schedulableWUs = readyWUs.filter((wu) => !deferredIds.has(wu.id));
1596
+ const schedulableMap = new Map(schedulableWUs.map((wu) => [wu.id, wu]));
1597
+ const schedulableIds = new Set(schedulableMap.keys());
1598
+ if (schedulableIds.size === 0) {
1599
+ return { waves: [], skipped, skippedWithReasons, deferred };
1600
+ }
1601
+ // WU-1326: Build set of lanes currently occupied based on policy
1602
+ // Track which lanes are occupied by in_progress or blocked WUs
1603
+ const lanesOccupiedByInProgress = new Set();
1604
+ const lanesOccupiedByBlocked = new Set();
1605
+ for (const wu of wus) {
1606
+ const status = wu.doc.status ?? 'unknown';
1607
+ const lane = wu.doc.lane;
1608
+ if (lane) {
1609
+ if (status === WU_STATUS.IN_PROGRESS) {
1610
+ lanesOccupiedByInProgress.add(lane);
1611
+ }
1612
+ else if (status === WU_STATUS.BLOCKED) {
1613
+ lanesOccupiedByBlocked.add(lane);
1614
+ }
1615
+ }
1616
+ }
1617
+ // Build waves using Kahn's algorithm (topological sort by levels)
1618
+ // WU-1326: Enforce lane WIP based on lock_policy
1619
+ const waves = [];
1620
+ const remaining = new Set(schedulableIds);
1621
+ const completed = new Set(skipped);
1622
+ // Also treat stamped external deps as completed
1623
+ for (const wu of wus) {
1624
+ const blockers = getAllDependencies(wu.doc);
1625
+ for (const blockerId of blockers) {
1626
+ if (!allWuIds.has(blockerId) && hasStamp(blockerId)) {
1627
+ completed.add(blockerId);
1628
+ }
1629
+ }
1630
+ }
1631
+ while (remaining.size > 0) {
1632
+ const wave = [];
1633
+ const lanesInWave = new Set(); // Track lanes used in this wave
1634
+ const deferredToNextWave = []; // WUs that could run but lane is occupied
1635
+ for (const id of remaining) {
1636
+ const wu = schedulableMap.get(id);
1637
+ if (!wu)
1638
+ continue; // Should not happen - remaining only contains valid IDs
1639
+ const blockers = getAllDependencies(wu.doc);
1640
+ // Check if all blockers are either done or completed in previous waves
1641
+ const allBlockersDone = blockers.every((blockerId) => completed.has(blockerId));
1642
+ if (allBlockersDone) {
1643
+ const lane = wu.doc.lane ?? '';
1644
+ // WU-1326: Get lock_policy for this lane
1645
+ const policy = getLockPolicyForLane(lane, laneConfigs);
1646
+ // WU-1326: Check if lane is already occupied in this wave
1647
+ // Skip this check when policy=none (allows unlimited parallel WUs in same lane)
1648
+ if (policy !== 'none' && lanesInWave.has(lane)) {
1649
+ // Defer to next wave (lane conflict within this wave)
1650
+ deferredToNextWave.push(wu);
1651
+ continue;
1652
+ }
1653
+ // WU-1326: Check if lane is occupied by existing WUs based on policy
1654
+ // policy=none: laneBlocked stays false (no WIP checking)
1655
+ // policy=active: only in_progress blocks
1656
+ // policy=all: both in_progress and blocked block
1657
+ let laneBlocked = false;
1658
+ if (policy === 'active') {
1659
+ // Only in_progress WUs block the lane
1660
+ laneBlocked = lanesOccupiedByInProgress.has(lane);
1661
+ }
1662
+ else if (policy === 'all') {
1663
+ // policy === 'all' (default): both in_progress and blocked block lane
1664
+ laneBlocked = lanesOccupiedByInProgress.has(lane) || lanesOccupiedByBlocked.has(lane);
1665
+ }
1666
+ // policy === 'none': laneBlocked remains false
1667
+ if (laneBlocked) {
1668
+ // Lane is occupied by existing WU based on policy
1669
+ deferredToNextWave.push(wu);
1670
+ }
1671
+ else {
1672
+ wave.push(wu);
1673
+ lanesInWave.add(lane);
1674
+ }
1675
+ }
1676
+ }
1677
+ // Deadlock detection: if no WUs can be scheduled but remaining exist
1678
+ if (wave.length === 0 && remaining.size > 0 && deferredToNextWave.length === 0) {
1679
+ const stuckIds = Array.from(remaining);
1680
+ throw createError(ErrorCodes.VALIDATION_ERROR, `Circular or unresolvable dependencies detected. Stuck WUs: ${stuckIds.join(', ')}`, { stuckIds });
1681
+ }
1682
+ // Add wave and mark WUs as completed
1683
+ if (wave.length > 0) {
1684
+ waves.push(wave);
1685
+ for (const wu of wave) {
1686
+ remaining.delete(wu.id);
1687
+ completed.add(wu.id);
1688
+ }
1689
+ }
1690
+ // Add deferred WUs back to remaining for next wave (if wave had items)
1691
+ // If wave was empty but we have deferred items, we need to make progress
1692
+ if (wave.length === 0 && deferredToNextWave.length > 0) {
1693
+ // Schedule one deferred WU per lane to make progress
1694
+ const processedLanes = new Set();
1695
+ for (const wu of deferredToNextWave) {
1696
+ const lane = wu.doc.lane ?? '';
1697
+ if (!processedLanes.has(lane)) {
1698
+ wave.push(wu);
1699
+ processedLanes.add(lane);
1700
+ }
1701
+ }
1702
+ if (wave.length > 0) {
1703
+ waves.push(wave);
1704
+ for (const wu of wave) {
1705
+ remaining.delete(wu.id);
1706
+ completed.add(wu.id);
1707
+ }
1708
+ }
1709
+ }
1710
+ }
1711
+ return { waves, skipped, skippedWithReasons, deferred };
1712
+ }
1713
+ /**
1714
+ * WU-1326: Get lane availability respecting lock_policy.
1715
+ *
1716
+ * Returns availability status for each lane based on current WU states
1717
+ * and configured lock_policy.
1718
+ *
1719
+ * @param {Array<{id: string, doc: object}>} wus - WUs to check
1720
+ * @param {LockPolicyOptions} options - Lock policy options
1721
+ * @returns {Record<string, LaneAvailabilityResult>} Lane availability map
1722
+ */
1723
+ // eslint-disable-next-line sonarjs/cognitive-complexity -- lane availability logic with multiple policy branches
1724
+ export function getLaneAvailability(wus, options = {}) {
1725
+ const { laneConfigs = {} } = options;
1726
+ const result = {};
1727
+ // Group WUs by lane
1728
+ const wusByLane = new Map();
1729
+ for (const wu of wus) {
1730
+ const lane = wu.doc.lane;
1731
+ if (lane) {
1732
+ const laneWUs = wusByLane.get(lane);
1733
+ if (laneWUs) {
1734
+ laneWUs.push(wu);
1735
+ }
1736
+ else {
1737
+ wusByLane.set(lane, [wu]);
1738
+ }
1739
+ }
1740
+ }
1741
+ // Calculate availability for each lane
1742
+ for (const [lane, laneWUs] of wusByLane) {
1743
+ const policy = getLockPolicyForLane(lane, laneConfigs);
1744
+ let inProgressCount = 0;
1745
+ let blockedCount = 0;
1746
+ let occupiedBy;
1747
+ for (const wu of laneWUs) {
1748
+ const status = wu.doc.status ?? 'unknown';
1749
+ if (status === WU_STATUS.IN_PROGRESS) {
1750
+ inProgressCount++;
1751
+ if (!occupiedBy) {
1752
+ occupiedBy = wu.id;
1753
+ }
1754
+ }
1755
+ else if (status === WU_STATUS.BLOCKED) {
1756
+ blockedCount++;
1757
+ // Only set occupiedBy for blocked if policy=all
1758
+ if (policy === 'all' && !occupiedBy) {
1759
+ occupiedBy = wu.id;
1760
+ }
1761
+ }
1762
+ }
1763
+ // Determine availability based on policy
1764
+ let available = false;
1765
+ if (policy === 'none') {
1766
+ // No WIP checking - always available
1767
+ available = true;
1768
+ occupiedBy = undefined;
1769
+ }
1770
+ else if (policy === 'active') {
1771
+ // Only in_progress blocks
1772
+ available = inProgressCount === 0;
1773
+ if (available) {
1774
+ occupiedBy = undefined;
1775
+ }
1776
+ }
1777
+ else {
1778
+ // policy === 'all': both in_progress and blocked block
1779
+ available = inProgressCount === 0 && blockedCount === 0;
1780
+ }
1781
+ result[lane] = {
1782
+ available,
1783
+ policy,
1784
+ occupiedBy,
1785
+ blockedCount,
1786
+ inProgressCount,
1787
+ };
1788
+ }
1789
+ return result;
1790
+ }
1416
1791
  export { LOG_PREFIX };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumenflow/initiatives",
3
- "version": "2.3.2",
3
+ "version": "2.5.0",
4
4
  "description": "Initiative tracking for LumenFlow workflow framework - multi-phase project coordination",
5
5
  "keywords": [
6
6
  "lumenflow",
@@ -41,7 +41,7 @@
41
41
  "dependencies": {
42
42
  "yaml": "^2.8.2",
43
43
  "zod": "^4.3.5",
44
- "@lumenflow/core": "2.3.2"
44
+ "@lumenflow/core": "2.5.0"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@vitest/coverage-v8": "^4.0.17",