@lumenflow/initiatives 2.2.2 → 2.3.2

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/index.d.ts CHANGED
@@ -6,5 +6,6 @@ export * from './initiative-constants.js';
6
6
  export * from './initiative-orchestrator.js';
7
7
  export * from './initiative-paths.js';
8
8
  export * from './initiative-schema.js';
9
+ export * from './initiative-validation.js';
9
10
  export * from './initiative-validator.js';
10
11
  export * from './initiative-yaml.js';
package/dist/index.js CHANGED
@@ -6,5 +6,6 @@ export * from './initiative-constants.js';
6
6
  export * from './initiative-orchestrator.js';
7
7
  export * from './initiative-paths.js';
8
8
  export * from './initiative-schema.js';
9
+ export * from './initiative-validation.js';
9
10
  export * from './initiative-validator.js';
10
11
  export * from './initiative-yaml.js';
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Centralized constants for Initiative management scripts.
3
3
  *
4
- * Mirrors wu-constants.mjs pattern. Single source of truth for all
4
+ * Mirrors wu-constants.ts pattern. Single source of truth for all
5
5
  * initiative-related magic strings, patterns, and enums.
6
6
  *
7
7
  * @example
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Centralized constants for Initiative management scripts.
3
3
  *
4
- * Mirrors wu-constants.mjs pattern. Single source of truth for all
4
+ * Mirrors wu-constants.ts pattern. Single source of truth for all
5
5
  * initiative-related magic strings, patterns, and enums.
6
6
  *
7
7
  * @example
@@ -15,9 +15,9 @@
15
15
  * - Wave manifest files for idempotent resumption
16
16
  * - Compact output for token discipline
17
17
  *
18
- * @see {@link tools/orchestrate-initiative.mjs} - CLI entry point
19
- * @see {@link tools/lib/initiative-yaml.mjs} - Initiative loading
20
- * @see {@link tools/lib/dependency-graph.mjs} - Dependency graph utilities
18
+ * @see {@link packages/@lumenflow/cli/src/orchestrate-initiative.ts} - CLI entry point
19
+ * @see {@link packages/@lumenflow/cli/src/lib/initiative-yaml.ts} - Initiative loading
20
+ * @see {@link packages/@lumenflow/cli/src/lib/dependency-graph.ts} - Dependency graph utilities
21
21
  */
22
22
  import type { InitiativeDoc, WUEntry } from './initiative-yaml.js';
23
23
  /**
@@ -146,6 +146,52 @@ export declare const CHECKPOINT_AUTO_THRESHOLDS: {
146
146
  /** Auto-enable checkpoint mode if wave count exceeds this (>2 = 3+) */
147
147
  WAVE_COUNT: number;
148
148
  };
149
+ /**
150
+ * WU-1200: Get the status string used in wave manifests for WUs.
151
+ *
152
+ * Returns 'queued' instead of 'spawned' to prevent confusion.
153
+ * A WU is 'queued' in the manifest when the spawn prompt is output,
154
+ * but it's not actually 'spawned' until an agent claims it.
155
+ *
156
+ * @returns {string} The manifest WU status ('queued')
157
+ */
158
+ export declare function getManifestWUStatus(): string;
159
+ /**
160
+ * WU-1200: Check if a WU has actually been spawned (agent launched).
161
+ *
162
+ * This checks the WU YAML status, not the wave manifest. A WU is considered
163
+ * "actually spawned" only if:
164
+ * - Its YAML status is 'in_progress' (agent has claimed it)
165
+ * - OR its YAML status is 'done' (agent has completed it)
166
+ *
167
+ * Wave manifests can have stale 'spawned' statuses from previous runs where
168
+ * the prompt was output but no agent was ever invoked. This function provides
169
+ * the authoritative check based on YAML status.
170
+ *
171
+ * @param {string} wuId - WU ID (e.g., 'WU-001')
172
+ * @returns {boolean} True if the WU is actually in progress or done
173
+ */
174
+ export declare function isWUActuallySpawned(wuId: string): boolean;
175
+ /**
176
+ * WU-1200: Get spawn candidates with YAML status verification.
177
+ *
178
+ * Filters WUs to find candidates that can be spawned, checking YAML status
179
+ * instead of relying solely on wave manifests. This prevents stale manifests
180
+ * from blocking new orchestration runs.
181
+ *
182
+ * A WU is a spawn candidate if:
183
+ * - Its YAML status is 'ready' (not in_progress, done, blocked, etc.)
184
+ * - It's in the provided WU list (part of the initiative)
185
+ *
186
+ * This function ignores wave manifest status because:
187
+ * - Manifests can be stale (prompt output but agent never launched)
188
+ * - YAML status is the authoritative source of truth
189
+ *
190
+ * @param {string} _initId - Initiative ID (for logging/context, not used for filtering)
191
+ * @param {Array<{id: string, doc: object}>} wus - WUs to filter
192
+ * @returns {Array<{id: string, doc: object}>} WUs that can be spawned
193
+ */
194
+ export declare function getSpawnCandidatesWithYAMLCheck(_initId: string, wus: WUEntry[]): WUEntry[];
149
195
  /**
150
196
  * Load initiative and its WUs.
151
197
  *
@@ -185,6 +231,13 @@ export declare function loadMultipleInitiatives(initRefs: string[]): WUEntry[];
185
231
  * @throws {Error} If circular dependencies detected
186
232
  */
187
233
  export declare function buildExecutionPlan(wus: WUEntry[]): ExecutionPlan;
234
+ /**
235
+ * Build execution plan from WUs asynchronously.
236
+ *
237
+ * @param {Array<{id: string, doc: object}>} wus - WUs to plan
238
+ * @returns {Promise<ExecutionPlan>}
239
+ */
240
+ export declare function buildExecutionPlanAsync(wus: WUEntry[]): Promise<ExecutionPlan>;
188
241
  /**
189
242
  * WU-1828: Determine if checkpoint mode should be auto-enabled based on initiative size.
190
243
  *
@@ -199,6 +252,13 @@ export declare function buildExecutionPlan(wus: WUEntry[]): ExecutionPlan;
199
252
  * @returns {{autoEnabled: boolean, reason: string, pendingCount: number, waveCount: number}}
200
253
  */
201
254
  export declare function shouldAutoEnableCheckpoint(wus: WUEntry[]): AutoCheckpointResult;
255
+ /**
256
+ * WU-1828: Determine if checkpoint mode should be auto-enabled based on initiative size asynchronously.
257
+ *
258
+ * @param {Array<{id: string, doc: object}>} wus - WUs to analyse
259
+ * @returns {Promise<{autoEnabled: boolean, reason: string, pendingCount: number, waveCount: number}>}
260
+ */
261
+ export declare function shouldAutoEnableCheckpointAsync(wus: WUEntry[]): Promise<AutoCheckpointResult>;
202
262
  /**
203
263
  * WU-1828: Resolve checkpoint mode from CLI flags and auto-detection.
204
264
  * WU-2430: Updated to suppress auto-detection in dry-run mode.
@@ -214,6 +274,14 @@ export declare function shouldAutoEnableCheckpoint(wus: WUEntry[]): AutoCheckpoi
214
274
  * @returns {{enabled: boolean, source: 'explicit'|'override'|'auto'|'dryrun', reason?: string}}
215
275
  */
216
276
  export declare function resolveCheckpointMode(options: CheckpointOptions, wus: WUEntry[]): CheckpointModeResult;
277
+ /**
278
+ * WU-1828: Resolve checkpoint mode from CLI flags and auto-detection asynchronously.
279
+ *
280
+ * @param {{checkpointPerWave?: boolean, noCheckpoint?: boolean, dryRun?: boolean}} options - CLI options
281
+ * @param {Array<{id: string, doc: object}>} wus - WUs for auto-detection
282
+ * @returns {Promise<{enabled: boolean, source: 'explicit'|'override'|'auto'|'dryrun', reason?: string}>}
283
+ */
284
+ export declare function resolveCheckpointModeAsync(options: CheckpointOptions, wus: WUEntry[]): Promise<CheckpointModeResult>;
217
285
  /**
218
286
  * Get bottleneck WUs from a set of WUs based on how many downstream WUs they block.
219
287
  * A bottleneck is a WU that blocks multiple other WUs.
@@ -256,11 +324,12 @@ export declare function calculateProgress(wus: WUEntry[]): ProgressStats;
256
324
  export declare function formatProgress(progress: ProgressStats): string;
257
325
  /**
258
326
  * WU-2040: Filter WUs by dependency stamp status.
327
+ * WU-1251: Now checks both blocked_by AND dependencies arrays.
259
328
  *
260
- * A WU is only spawnable if ALL its blocked_by dependencies have stamps.
329
+ * A WU is only spawnable if ALL its dependencies have stamps.
261
330
  * This implements the wait-for-completion pattern per Anthropic multi-agent research.
262
331
  *
263
- * @param {Array<{id: string, doc: {blocked_by?: string[], lane: string, status: string}}>} candidates - WU candidates
332
+ * @param {Array<{id: string, doc: {blocked_by?: string[], dependencies?: string[], lane: string, status: string}}>} candidates - WU candidates
264
333
  * @returns {{spawnable: Array<object>, blocked: Array<object>, blockingDeps: string[], waitingMessage: string}}
265
334
  */
266
335
  export declare function filterByDependencyStamps(candidates: WUEntry[]): DependencyFilterResult;
@@ -15,14 +15,34 @@
15
15
  * - Wave manifest files for idempotent resumption
16
16
  * - Compact output for token discipline
17
17
  *
18
- * @see {@link tools/orchestrate-initiative.mjs} - CLI entry point
19
- * @see {@link tools/lib/initiative-yaml.mjs} - Initiative loading
20
- * @see {@link tools/lib/dependency-graph.mjs} - Dependency graph utilities
18
+ * @see {@link packages/@lumenflow/cli/src/orchestrate-initiative.ts} - CLI entry point
19
+ * @see {@link packages/@lumenflow/cli/src/lib/initiative-yaml.ts} - Initiative loading
20
+ * @see {@link packages/@lumenflow/cli/src/lib/dependency-graph.ts} - Dependency graph utilities
21
21
  */
22
22
  import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync } from 'node:fs';
23
23
  import { join } from 'node:path';
24
24
  import { findInitiative, getInitiativeWUs } from './initiative-yaml.js';
25
- import { buildDependencyGraph, validateGraph } from '@lumenflow/core/lib/dependency-graph.js';
25
+ import { buildDependencyGraph, buildDependencyGraphAsync, validateGraph, } from '@lumenflow/core/lib/dependency-graph.js';
26
+ /**
27
+ * WU-1251: Helper to get all dependencies from a WU doc.
28
+ *
29
+ * Combines both `blocked_by` and `dependencies` arrays for dependency resolution.
30
+ * The WU YAML schema supports both:
31
+ * - `blocked_by`: Legacy/explicit blockers
32
+ * - `dependencies`: Semantic dependencies on other WUs
33
+ *
34
+ * Both arrays represent the same concept: WUs that must complete before this WU can start.
35
+ *
36
+ * @param {object} doc - WU document
37
+ * @returns {string[]} Combined list of all dependency WU IDs (deduplicated)
38
+ */
39
+ function getAllDependencies(doc) {
40
+ const blockedBy = doc.blocked_by ?? [];
41
+ const dependencies = doc.dependencies ?? [];
42
+ // Combine and deduplicate
43
+ const allDeps = new Set([...blockedBy, ...dependencies]);
44
+ return Array.from(allDeps);
45
+ }
26
46
  import { createError, ErrorCodes } from '@lumenflow/core/lib/error-handler.js';
27
47
  import { WU_STATUS, STRING_LITERALS } from '@lumenflow/core/lib/wu-constants.js';
28
48
  import { WU_PATHS } from '@lumenflow/core/lib/wu-paths.js';
@@ -72,6 +92,88 @@ export const CHECKPOINT_AUTO_THRESHOLDS = {
72
92
  /** Auto-enable checkpoint mode if wave count exceeds this (>2 = 3+) */
73
93
  WAVE_COUNT: 2,
74
94
  };
95
+ /**
96
+ * WU-1200: Wave manifest WU status constant.
97
+ *
98
+ * Changed from 'spawned' to 'queued' to prevent confusion.
99
+ * 'spawned' implies an agent was launched, but the manifest is written
100
+ * BEFORE an agent is actually invoked (when the prompt is output, not when
101
+ * the user copies and executes it).
102
+ *
103
+ * Using 'queued' makes it clear the WU is ready to be spawned but not yet running.
104
+ */
105
+ const MANIFEST_WU_STATUS = 'queued';
106
+ /**
107
+ * WU-1200: Get the status string used in wave manifests for WUs.
108
+ *
109
+ * Returns 'queued' instead of 'spawned' to prevent confusion.
110
+ * A WU is 'queued' in the manifest when the spawn prompt is output,
111
+ * but it's not actually 'spawned' until an agent claims it.
112
+ *
113
+ * @returns {string} The manifest WU status ('queued')
114
+ */
115
+ export function getManifestWUStatus() {
116
+ return MANIFEST_WU_STATUS;
117
+ }
118
+ /**
119
+ * WU-1200: Check if a WU has actually been spawned (agent launched).
120
+ *
121
+ * This checks the WU YAML status, not the wave manifest. A WU is considered
122
+ * "actually spawned" only if:
123
+ * - Its YAML status is 'in_progress' (agent has claimed it)
124
+ * - OR its YAML status is 'done' (agent has completed it)
125
+ *
126
+ * Wave manifests can have stale 'spawned' statuses from previous runs where
127
+ * the prompt was output but no agent was ever invoked. This function provides
128
+ * the authoritative check based on YAML status.
129
+ *
130
+ * @param {string} wuId - WU ID (e.g., 'WU-001')
131
+ * @returns {boolean} True if the WU is actually in progress or done
132
+ */
133
+ export function isWUActuallySpawned(wuId) {
134
+ const wuPath = WU_PATHS.WU(wuId);
135
+ if (!existsSync(wuPath)) {
136
+ // WU file not found - can't determine status, assume not spawned
137
+ return false;
138
+ }
139
+ try {
140
+ const text = readFileSync(wuPath, 'utf8');
141
+ const doc = parseYAML(text);
142
+ const status = doc.status ?? 'unknown';
143
+ // WU is "actually spawned" if status indicates active or completed work
144
+ return status === WU_STATUS.IN_PROGRESS || status === WU_STATUS.DONE;
145
+ }
146
+ catch {
147
+ // Error reading/parsing WU file - assume not spawned
148
+ return false;
149
+ }
150
+ }
151
+ /**
152
+ * WU-1200: Get spawn candidates with YAML status verification.
153
+ *
154
+ * Filters WUs to find candidates that can be spawned, checking YAML status
155
+ * instead of relying solely on wave manifests. This prevents stale manifests
156
+ * from blocking new orchestration runs.
157
+ *
158
+ * A WU is a spawn candidate if:
159
+ * - Its YAML status is 'ready' (not in_progress, done, blocked, etc.)
160
+ * - It's in the provided WU list (part of the initiative)
161
+ *
162
+ * This function ignores wave manifest status because:
163
+ * - Manifests can be stale (prompt output but agent never launched)
164
+ * - YAML status is the authoritative source of truth
165
+ *
166
+ * @param {string} _initId - Initiative ID (for logging/context, not used for filtering)
167
+ * @param {Array<{id: string, doc: object}>} wus - WUs to filter
168
+ * @returns {Array<{id: string, doc: object}>} WUs that can be spawned
169
+ */
170
+ export function getSpawnCandidatesWithYAMLCheck(_initId, wus) {
171
+ // Filter to only 'ready' status WUs based on YAML, not manifest
172
+ return wus.filter((wu) => {
173
+ const status = wu.doc.status ?? 'unknown';
174
+ return status === WU_STATUS.READY;
175
+ });
176
+ }
75
177
  /**
76
178
  * Load initiative and its WUs.
77
179
  *
@@ -190,7 +292,8 @@ export function buildExecutionPlan(wus) {
190
292
  reasonSet.add(reason);
191
293
  };
192
294
  for (const wu of readyWUs) {
193
- const blockers = wu.doc.blocked_by ?? [];
295
+ // WU-1251: Use getAllDependencies to combine blocked_by and dependencies arrays
296
+ const blockers = getAllDependencies(wu.doc);
194
297
  const externalBlockers = blockers.filter((blockerId) => !allWuIds.has(blockerId));
195
298
  const internalBlockers = blockers.filter((blockerId) => allWuIds.has(blockerId));
196
299
  if (externalBlockers.length > 0) {
@@ -225,7 +328,8 @@ export function buildExecutionPlan(wus) {
225
328
  if (deferredIds.has(wu.id)) {
226
329
  continue;
227
330
  }
228
- const blockers = wu.doc.blocked_by || [];
331
+ // WU-1251: Use getAllDependencies to combine blocked_by and dependencies arrays
332
+ const blockers = getAllDependencies(wu.doc);
229
333
  const deferredInternal = blockers.filter((blockerId) => allWuIds.has(blockerId) && deferredIds.has(blockerId));
230
334
  if (deferredInternal.length > 0) {
231
335
  const details = deferredInternal.map((blockerId) => {
@@ -262,7 +366,8 @@ export function buildExecutionPlan(wus) {
262
366
  const completed = new Set(skipped); // Treat done WUs as completed for dependency resolution
263
367
  // Also treat stamped external deps as completed
264
368
  for (const wu of wus) {
265
- const blockers = wu.doc.blocked_by || [];
369
+ // WU-1251: Use getAllDependencies to combine blocked_by and dependencies arrays
370
+ const blockers = getAllDependencies(wu.doc);
266
371
  for (const blockerId of blockers) {
267
372
  if (!allWuIds.has(blockerId) && hasStamp(blockerId)) {
268
373
  completed.add(blockerId);
@@ -275,7 +380,193 @@ export function buildExecutionPlan(wus) {
275
380
  const deferredToNextWave = []; // WUs that could run but lane is occupied
276
381
  for (const id of remaining) {
277
382
  const wu = schedulableMap.get(id);
278
- const blockers = wu.doc.blocked_by || [];
383
+ // WU-1251: Use getAllDependencies to combine blocked_by and dependencies arrays
384
+ const blockers = getAllDependencies(wu.doc);
385
+ // Check if all blockers are either done or completed in previous waves
386
+ const allBlockersDone = blockers.every((blockerId) => completed.has(blockerId));
387
+ if (allBlockersDone) {
388
+ // WU-1618: Check if lane is already occupied in this wave
389
+ const lane = wu.doc.lane;
390
+ if (lanesInWave.has(lane)) {
391
+ // Defer to next wave (lane conflict)
392
+ deferredToNextWave.push(wu);
393
+ }
394
+ else {
395
+ wave.push(wu);
396
+ lanesInWave.add(lane);
397
+ }
398
+ }
399
+ }
400
+ // Deadlock detection: if no WUs can be scheduled but remaining exist
401
+ // WU-1618: Account for deferred WUs (they can run in next wave, not stuck)
402
+ if (wave.length === 0 && remaining.size > 0 && deferredToNextWave.length === 0) {
403
+ const stuckIds = Array.from(remaining);
404
+ throw createError(ErrorCodes.VALIDATION_ERROR, `Circular or unresolvable dependencies detected. Stuck WUs: ${stuckIds.join(', ')}`, { stuckIds });
405
+ }
406
+ // Add wave and mark WUs as completed
407
+ waves.push(wave);
408
+ for (const wu of wave) {
409
+ remaining.delete(wu.id);
410
+ completed.add(wu.id);
411
+ }
412
+ }
413
+ return { waves, skipped, skippedWithReasons, deferred };
414
+ }
415
+ /**
416
+ * Build execution plan from WUs asynchronously.
417
+ *
418
+ * @param {Array<{id: string, doc: object}>} wus - WUs to plan
419
+ * @returns {Promise<ExecutionPlan>}
420
+ */
421
+ export async function buildExecutionPlanAsync(wus) {
422
+ // WU-2430: Enhanced categorisation of WUs
423
+ const skipped = []; // IDs of done WUs (backwards compat)
424
+ const skippedWithReasons = []; // WU-2430: Non-ready WUs with reasons
425
+ const deferred = []; // WU-2430: Ready WUs waiting on external blockers
426
+ const doneStatuses = new Set([WU_STATUS.DONE, WU_STATUS.COMPLETED]);
427
+ // Categorise WUs by status
428
+ for (const wu of wus) {
429
+ const status = wu.doc.status ?? 'unknown';
430
+ if (doneStatuses.has(status)) {
431
+ skipped.push(wu.id);
432
+ }
433
+ else if (status !== WU_STATUS.READY) {
434
+ skippedWithReasons.push({ id: wu.id, reason: `status: ${status}` });
435
+ }
436
+ }
437
+ // WU-2430: Only ready WUs are candidates for execution
438
+ const readyWUs = wus.filter((wu) => wu.doc.status === WU_STATUS.READY);
439
+ if (readyWUs.length === 0) {
440
+ return { waves: [], skipped, skippedWithReasons, deferred };
441
+ }
442
+ // Build a map for quick lookup
443
+ const wuMap = new Map(readyWUs.map((wu) => [wu.id, wu]));
444
+ const wuIds = new Set(wuMap.keys());
445
+ const allWuMap = new Map(wus.map((wu) => [wu.id, wu]));
446
+ const allWuIds = new Set(allWuMap.keys());
447
+ // Build dependency graph for validation (check cycles)
448
+ const graph = await buildDependencyGraphAsync();
449
+ const { cycles } = validateGraph(graph);
450
+ // Filter cycles to only those involving our WUs
451
+ const relevantCycles = cycles.filter((cycle) => cycle.some((id) => wuIds.has(id)));
452
+ if (relevantCycles.length > 0) {
453
+ const cycleStr = relevantCycles.map((c) => c.join(' → ')).join('; ');
454
+ throw createError(ErrorCodes.VALIDATION_ERROR, `Circular dependencies detected: ${cycleStr}`, {
455
+ cycles: relevantCycles,
456
+ });
457
+ }
458
+ // WU-2430: Check for external blockers without stamps
459
+ // A WU with blocked_by dependencies that are NOT in the initiative
460
+ // and do NOT have stamps should be deferred
461
+ const deferredIds = new Set();
462
+ const deferredReasons = new Map();
463
+ const deferredBlockers = new Map();
464
+ const addDeferredEntry = (wuId, blockers, reason) => {
465
+ deferredIds.add(wuId);
466
+ if (!deferredReasons.has(wuId)) {
467
+ deferredReasons.set(wuId, new Set());
468
+ }
469
+ if (!deferredBlockers.has(wuId)) {
470
+ deferredBlockers.set(wuId, new Set());
471
+ }
472
+ const reasonSet = deferredReasons.get(wuId);
473
+ const blockerSet = deferredBlockers.get(wuId);
474
+ for (const blockerId of blockers) {
475
+ blockerSet.add(blockerId);
476
+ }
477
+ reasonSet.add(reason);
478
+ };
479
+ for (const wu of readyWUs) {
480
+ // WU-1251: Use getAllDependencies to combine blocked_by and dependencies arrays
481
+ const blockers = getAllDependencies(wu.doc);
482
+ const externalBlockers = blockers.filter((blockerId) => !allWuIds.has(blockerId));
483
+ const internalBlockers = blockers.filter((blockerId) => allWuIds.has(blockerId));
484
+ if (externalBlockers.length > 0) {
485
+ // Check if any external blockers lack stamps
486
+ const unstampedBlockers = externalBlockers.filter((blockerId) => !hasStamp(blockerId));
487
+ if (unstampedBlockers.length > 0) {
488
+ addDeferredEntry(wu.id, unstampedBlockers, `waiting for external: ${unstampedBlockers.join(', ')}`);
489
+ }
490
+ }
491
+ if (internalBlockers.length > 0) {
492
+ const nonReadyInternal = internalBlockers.filter((blockerId) => {
493
+ const blocker = allWuMap.get(blockerId);
494
+ const status = blocker?.doc?.status ?? 'unknown';
495
+ if (status === WU_STATUS.READY) {
496
+ return false;
497
+ }
498
+ return !doneStatuses.has(status);
499
+ });
500
+ if (nonReadyInternal.length > 0) {
501
+ const details = nonReadyInternal.map((blockerId) => {
502
+ const status = allWuMap.get(blockerId)?.doc?.status ?? 'unknown';
503
+ return `${blockerId} (status: ${status})`;
504
+ });
505
+ addDeferredEntry(wu.id, nonReadyInternal, `waiting for internal: ${details.join(', ')}`);
506
+ }
507
+ }
508
+ }
509
+ let hasNewDeferral = true;
510
+ while (hasNewDeferral) {
511
+ hasNewDeferral = false;
512
+ for (const wu of readyWUs) {
513
+ if (deferredIds.has(wu.id)) {
514
+ continue;
515
+ }
516
+ // WU-1251: Use getAllDependencies to combine blocked_by and dependencies arrays
517
+ const blockers = getAllDependencies(wu.doc);
518
+ const deferredInternal = blockers.filter((blockerId) => allWuIds.has(blockerId) && deferredIds.has(blockerId));
519
+ if (deferredInternal.length > 0) {
520
+ const details = deferredInternal.map((blockerId) => {
521
+ const status = allWuMap.get(blockerId)?.doc?.status ?? 'unknown';
522
+ return `${blockerId} (status: ${status})`;
523
+ });
524
+ addDeferredEntry(wu.id, deferredInternal, `waiting for internal: ${details.join(', ')}`);
525
+ hasNewDeferral = true;
526
+ }
527
+ }
528
+ }
529
+ for (const wu of readyWUs) {
530
+ if (deferredIds.has(wu.id)) {
531
+ const blockerSet = deferredBlockers.get(wu.id) || new Set();
532
+ const reasonSet = deferredReasons.get(wu.id) || new Set();
533
+ deferred.push({
534
+ id: wu.id,
535
+ blockedBy: Array.from(blockerSet),
536
+ reason: reasonSet.size > 0 ? Array.from(reasonSet).join('; ') : 'waiting for dependencies',
537
+ });
538
+ }
539
+ }
540
+ // Remove deferred WUs from candidates
541
+ const schedulableWUs = readyWUs.filter((wu) => !deferredIds.has(wu.id));
542
+ const schedulableMap = new Map(schedulableWUs.map((wu) => [wu.id, wu]));
543
+ const schedulableIds = new Set(schedulableMap.keys());
544
+ if (schedulableIds.size === 0) {
545
+ return { waves: [], skipped, skippedWithReasons, deferred };
546
+ }
547
+ // Build waves using Kahn's algorithm (topological sort by levels)
548
+ // WU-1618: Also enforce lane WIP=1 constraint (no two WUs with same lane in same wave)
549
+ const waves = [];
550
+ const remaining = new Set(schedulableIds);
551
+ const completed = new Set(skipped); // Treat done WUs as completed for dependency resolution
552
+ // Also treat stamped external deps as completed
553
+ for (const wu of wus) {
554
+ // WU-1251: Use getAllDependencies to combine blocked_by and dependencies arrays
555
+ const blockers = getAllDependencies(wu.doc);
556
+ for (const blockerId of blockers) {
557
+ if (!allWuIds.has(blockerId) && hasStamp(blockerId)) {
558
+ completed.add(blockerId);
559
+ }
560
+ }
561
+ }
562
+ while (remaining.size > 0) {
563
+ const wave = [];
564
+ const lanesInWave = new Set(); // WU-1618: Track lanes used in this wave
565
+ const deferredToNextWave = []; // WUs that could run but lane is occupied
566
+ for (const id of remaining) {
567
+ const wu = schedulableMap.get(id);
568
+ // WU-1251: Use getAllDependencies to combine blocked_by and dependencies arrays
569
+ const blockers = getAllDependencies(wu.doc);
279
570
  // Check if all blockers are either done or completed in previous waves
280
571
  const allBlockersDone = blockers.every((blockerId) => completed.has(blockerId));
281
572
  if (allBlockersDone) {
@@ -360,6 +651,53 @@ export function shouldAutoEnableCheckpoint(wus) {
360
651
  waveCount,
361
652
  };
362
653
  }
654
+ /**
655
+ * WU-1828: Determine if checkpoint mode should be auto-enabled based on initiative size asynchronously.
656
+ *
657
+ * @param {Array<{id: string, doc: object}>} wus - WUs to analyse
658
+ * @returns {Promise<{autoEnabled: boolean, reason: string, pendingCount: number, waveCount: number}>}
659
+ */
660
+ export async function shouldAutoEnableCheckpointAsync(wus) {
661
+ // Count only pending WUs (not done)
662
+ const pendingWUs = wus.filter((wu) => wu.doc.status !== WU_STATUS.DONE);
663
+ const pendingCount = pendingWUs.length;
664
+ // Check WU count threshold first (faster check)
665
+ if (pendingCount > CHECKPOINT_AUTO_THRESHOLDS.WU_COUNT) {
666
+ return {
667
+ autoEnabled: true,
668
+ reason: `${pendingCount} pending WUs exceeds threshold (>${CHECKPOINT_AUTO_THRESHOLDS.WU_COUNT})`,
669
+ pendingCount,
670
+ waveCount: -1, // Not computed (early return)
671
+ };
672
+ }
673
+ // Only compute waves if WU count didn't trigger
674
+ if (pendingCount === 0) {
675
+ return {
676
+ autoEnabled: false,
677
+ reason: 'No pending WUs',
678
+ pendingCount: 0,
679
+ waveCount: 0,
680
+ };
681
+ }
682
+ // Build execution plan to count waves
683
+ const plan = await buildExecutionPlanAsync(wus);
684
+ const waveCount = plan.waves.length;
685
+ // Check wave count threshold
686
+ if (waveCount > CHECKPOINT_AUTO_THRESHOLDS.WAVE_COUNT) {
687
+ return {
688
+ autoEnabled: true,
689
+ reason: `${waveCount} waves exceeds threshold (>${CHECKPOINT_AUTO_THRESHOLDS.WAVE_COUNT})`,
690
+ pendingCount,
691
+ waveCount,
692
+ };
693
+ }
694
+ return {
695
+ autoEnabled: false,
696
+ reason: `${pendingCount} pending WUs and ${waveCount} waves within thresholds`,
697
+ pendingCount,
698
+ waveCount,
699
+ };
700
+ }
363
701
  /**
364
702
  * WU-1828: Resolve checkpoint mode from CLI flags and auto-detection.
365
703
  * WU-2430: Updated to suppress auto-detection in dry-run mode.
@@ -408,6 +746,47 @@ export function resolveCheckpointMode(options, wus) {
408
746
  reason: autoResult.reason,
409
747
  };
410
748
  }
749
+ /**
750
+ * WU-1828: Resolve checkpoint mode from CLI flags and auto-detection asynchronously.
751
+ *
752
+ * @param {{checkpointPerWave?: boolean, noCheckpoint?: boolean, dryRun?: boolean}} options - CLI options
753
+ * @param {Array<{id: string, doc: object}>} wus - WUs for auto-detection
754
+ * @returns {Promise<{enabled: boolean, source: 'explicit'|'override'|'auto'|'dryrun', reason?: string}>}
755
+ */
756
+ export async function resolveCheckpointModeAsync(options, wus) {
757
+ const { checkpointPerWave = false, noCheckpoint = false, dryRun = false } = options;
758
+ // Explicit enable via -c flag
759
+ if (checkpointPerWave) {
760
+ return {
761
+ enabled: true,
762
+ source: 'explicit',
763
+ reason: 'Enabled via -c/--checkpoint-per-wave flag',
764
+ };
765
+ }
766
+ // Explicit disable via --no-checkpoint flag
767
+ if (noCheckpoint) {
768
+ return {
769
+ enabled: false,
770
+ source: 'override',
771
+ reason: 'Disabled via --no-checkpoint flag',
772
+ };
773
+ }
774
+ // WU-2430: Dry-run suppresses auto-detection (preview should use polling mode)
775
+ if (dryRun) {
776
+ return {
777
+ enabled: false,
778
+ source: 'dryrun',
779
+ reason: 'Disabled in dry-run mode (preview uses polling mode)',
780
+ };
781
+ }
782
+ // Auto-detection
783
+ const autoResult = await shouldAutoEnableCheckpointAsync(wus);
784
+ return {
785
+ enabled: autoResult.autoEnabled,
786
+ source: 'auto',
787
+ reason: autoResult.reason,
788
+ };
789
+ }
411
790
  /**
412
791
  * Get bottleneck WUs from a set of WUs based on how many downstream WUs they block.
413
792
  * A bottleneck is a WU that blocks multiple other WUs.
@@ -425,7 +804,8 @@ export function getBottleneckWUs(wus, limit = 5) {
425
804
  }
426
805
  // Count how many WUs each WU blocks
427
806
  for (const wu of wus) {
428
- const blockers = wu.doc.blocked_by || [];
807
+ // WU-1251: Use getAllDependencies to combine blocked_by and dependencies arrays
808
+ const blockers = getAllDependencies(wu.doc);
429
809
  for (const blockerId of blockers) {
430
810
  if (blocksCounts.has(blockerId)) {
431
811
  blocksCounts.set(blockerId, blocksCounts.get(blockerId) + 1);
@@ -505,7 +885,8 @@ export function formatExecutionPlan(initiative, plan) {
505
885
  const wave = plan.waves[i];
506
886
  lines.push(`Wave ${i} (${wave.length} WU${wave.length !== 1 ? 's' : ''} in parallel):`);
507
887
  for (const wu of wave) {
508
- const blockers = wu.doc.blocked_by || [];
888
+ // WU-1251: Use getAllDependencies to combine blocked_by and dependencies arrays
889
+ const blockers = getAllDependencies(wu.doc);
509
890
  const blockerStr = blockers.length > 0 ? ` [blocked by: ${blockers.join(', ')}]` : '';
510
891
  // Mark bottleneck WUs (WU-1596)
511
892
  const isBottleneck = bottleneckWUs.some((b) => b.id === wu.id);
@@ -610,11 +991,12 @@ function hasStamp(wuId) {
610
991
  }
611
992
  /**
612
993
  * WU-2040: Filter WUs by dependency stamp status.
994
+ * WU-1251: Now checks both blocked_by AND dependencies arrays.
613
995
  *
614
- * A WU is only spawnable if ALL its blocked_by dependencies have stamps.
996
+ * A WU is only spawnable if ALL its dependencies have stamps.
615
997
  * This implements the wait-for-completion pattern per Anthropic multi-agent research.
616
998
  *
617
- * @param {Array<{id: string, doc: {blocked_by?: string[], lane: string, status: string}}>} candidates - WU candidates
999
+ * @param {Array<{id: string, doc: {blocked_by?: string[], dependencies?: string[], lane: string, status: string}}>} candidates - WU candidates
618
1000
  * @returns {{spawnable: Array<object>, blocked: Array<object>, blockingDeps: string[], waitingMessage: string}}
619
1001
  */
620
1002
  export function filterByDependencyStamps(candidates) {
@@ -622,7 +1004,8 @@ export function filterByDependencyStamps(candidates) {
622
1004
  const blocked = [];
623
1005
  const blockingDeps = new Set();
624
1006
  for (const wu of candidates) {
625
- const deps = wu.doc.blocked_by || [];
1007
+ // WU-1251: Use getAllDependencies to combine blocked_by and dependencies arrays
1008
+ const deps = getAllDependencies(wu.doc);
626
1009
  // Check if ALL dependencies have stamps
627
1010
  const unmetDeps = deps.filter((depId) => !hasStamp(depId));
628
1011
  if (unmetDeps.length === 0) {
@@ -684,7 +1067,7 @@ function getExistingWaveManifests(initId) {
684
1067
  * @param {string} initId - Initiative ID
685
1068
  * @returns {Set<string>} Set of WU IDs already in manifests
686
1069
  */
687
- function getSpawnedWUIds(initId) {
1070
+ function _getSpawnedWUIds(initId) {
688
1071
  const manifests = getExistingWaveManifests(initId);
689
1072
  const spawnedIds = new Set();
690
1073
  for (const manifest of manifests) {
@@ -753,25 +1136,29 @@ export function buildCheckpointWave(initRef, options = {}) {
753
1136
  }
754
1137
  const initId = initData.id;
755
1138
  const wus = getInitiativeWUs(initRef);
756
- // Get already spawned WU IDs from previous manifests
757
- const spawnedIds = getSpawnedWUIds(initId);
1139
+ // WU-1200: Check YAML status, not just wave manifests
1140
+ // Wave manifests can be stale (prompt output but agent never launched)
1141
+ // YAML status is the authoritative source of truth
758
1142
  // Filter to spawn candidates:
759
- // 1. status: ready only
1143
+ // 1. status: ready only (from YAML - authoritative)
760
1144
  // 2. No stamp exists (idempotency)
761
- // 3. Not already in a previous manifest
1145
+ // 3. WU is not actually spawned (YAML status not in_progress/done)
1146
+ //
1147
+ // Note: We no longer rely on wave manifests for exclusion because:
1148
+ // - Manifests can be stale (AC3: Stale wave manifests don't block new runs)
1149
+ // - YAML status is updated when an agent actually claims the WU
762
1150
  const readyCandidates = wus.filter((wu) => {
763
- // Only ready WUs
1151
+ // Only ready WUs (YAML status - authoritative)
764
1152
  if (wu.doc.status !== WU_STATUS.READY) {
765
1153
  return false;
766
1154
  }
767
- // Skip if stamp exists (highest precedence)
1155
+ // Skip if stamp exists (highest precedence - WU is complete)
768
1156
  if (hasStamp(wu.id)) {
769
1157
  return false;
770
1158
  }
771
- // Skip if already in previous manifest
772
- if (spawnedIds.has(wu.id)) {
773
- return false;
774
- }
1159
+ // WU-1200: Check YAML status, not manifest status
1160
+ // If YAML says 'ready', the WU hasn't been claimed yet, so it's spawnable
1161
+ // (even if a stale manifest says it was 'spawned')
775
1162
  return true;
776
1163
  });
777
1164
  // If no ready candidates, all work is done
@@ -805,6 +1192,8 @@ export function buildCheckpointWave(initRef, options = {}) {
805
1192
  // Determine wave number
806
1193
  const waveNum = getNextWaveNumber(initId);
807
1194
  // Build manifest
1195
+ // WU-1200: Use 'queued' status instead of 'spawned' to prevent confusion
1196
+ // 'queued' indicates the WU is ready to spawn, not that an agent was launched
808
1197
  const manifest = {
809
1198
  initiative: initId,
810
1199
  wave: waveNum,
@@ -812,7 +1201,7 @@ export function buildCheckpointWave(initRef, options = {}) {
812
1201
  wus: selectedWUs.map((wu) => ({
813
1202
  id: wu.id,
814
1203
  lane: wu.doc.lane,
815
- status: 'spawned',
1204
+ status: MANIFEST_WU_STATUS,
816
1205
  })),
817
1206
  lane_validation: 'pass',
818
1207
  done_criteria: 'All stamps exist in .lumenflow/stamps/',
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Centralized path constants for Initiative management scripts.
3
3
  *
4
- * Mirrors wu-paths.mjs pattern. Single source of truth for all
4
+ * Mirrors wu-paths.ts pattern. Single source of truth for all
5
5
  * initiative-related file paths.
6
6
  *
7
7
  * @example
@@ -2,7 +2,7 @@ import path from 'node:path';
2
2
  /**
3
3
  * Centralized path constants for Initiative management scripts.
4
4
  *
5
- * Mirrors wu-paths.mjs pattern. Single source of truth for all
5
+ * Mirrors wu-paths.ts pattern. Single source of truth for all
6
6
  * initiative-related file paths.
7
7
  *
8
8
  * @example
@@ -4,8 +4,8 @@
4
4
  * Zod schema for runtime validation of Initiative YAML structure.
5
5
  * Part of the Initiative System Phase 1 - Schema & Validation Foundation.
6
6
  *
7
- * @see {@link tools/validate.mjs} - Consumer (CI validation)
8
- * @see {@link tools/lib/initiative-validator.mjs} - Consumer (dependency graph validation)
7
+ * @see {@link packages/@lumenflow/cli/src/validate.ts} - Consumer (CI validation)
8
+ * @see {@link packages/@lumenflow/cli/src/lib/initiative-validator.ts} - Consumer (dependency graph validation)
9
9
  * @see {@link docs/04-operations/tasks/initiatives/} - Initiative YAML files
10
10
  */
11
11
  import { z } from 'zod';
@@ -4,8 +4,8 @@
4
4
  * Zod schema for runtime validation of Initiative YAML structure.
5
5
  * Part of the Initiative System Phase 1 - Schema & Validation Foundation.
6
6
  *
7
- * @see {@link tools/validate.mjs} - Consumer (CI validation)
8
- * @see {@link tools/lib/initiative-validator.mjs} - Consumer (dependency graph validation)
7
+ * @see {@link packages/@lumenflow/cli/src/validate.ts} - Consumer (CI validation)
8
+ * @see {@link packages/@lumenflow/cli/src/lib/initiative-validator.ts} - Consumer (dependency graph validation)
9
9
  * @see {@link docs/04-operations/tasks/initiatives/} - Initiative YAML files
10
10
  */
11
11
  import { z } from 'zod';
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Initiative Completeness Validation (WU-1211)
3
+ *
4
+ * Validates initiative completeness and determines when
5
+ * initiative status should auto-progress.
6
+ *
7
+ * Used by:
8
+ * - initiative:create (warns if description not provided)
9
+ * - wu:create --initiative (warns if initiative has no phases)
10
+ * - wu:claim (auto-progresses initiative status)
11
+ * - state:doctor (reports incomplete initiatives)
12
+ */
13
+ /**
14
+ * Result of initiative completeness validation
15
+ */
16
+ export interface InitiativeCompletenessResult {
17
+ /** Whether the initiative is valid (always true - issues are warnings not errors) */
18
+ valid: boolean;
19
+ /** Warning messages for incomplete fields */
20
+ warnings: string[];
21
+ }
22
+ /**
23
+ * Result of checking if initiative has phases defined
24
+ */
25
+ export interface InitiativePhaseCheck {
26
+ /** Whether the initiative has at least one phase */
27
+ hasPhases: boolean;
28
+ /** Warning message if no phases (null if phases exist) */
29
+ warning: string | null;
30
+ }
31
+ /**
32
+ * Result of checking if initiative status should progress
33
+ */
34
+ export interface InitiativeProgressCheck {
35
+ /** Whether the initiative status should progress */
36
+ shouldProgress: boolean;
37
+ /** The new status if shouldProgress is true */
38
+ newStatus: string | null;
39
+ }
40
+ /**
41
+ * Initiative document interface for validation
42
+ */
43
+ interface InitiativeDoc {
44
+ id?: string;
45
+ slug?: string;
46
+ title?: string;
47
+ description?: string;
48
+ status?: string;
49
+ phases?: Array<{
50
+ id: number;
51
+ title?: string;
52
+ status?: string;
53
+ }>;
54
+ success_metrics?: string[];
55
+ [key: string]: unknown;
56
+ }
57
+ /**
58
+ * WU document interface for validation
59
+ */
60
+ interface WUDoc {
61
+ id?: string;
62
+ status?: string;
63
+ initiative?: string;
64
+ [key: string]: unknown;
65
+ }
66
+ /**
67
+ * Validates initiative completeness and returns warnings for missing fields.
68
+ *
69
+ * This is soft validation - missing fields generate warnings, not errors.
70
+ * The initiative is still considered valid but incomplete.
71
+ *
72
+ * @param initiative - Initiative document to validate
73
+ * @returns Validation result with warnings for incomplete fields
74
+ *
75
+ * @example
76
+ * const result = validateInitiativeCompleteness(initiative);
77
+ * if (result.warnings.length > 0) {
78
+ * console.warn('Initiative is incomplete:', result.warnings);
79
+ * }
80
+ */
81
+ export declare function validateInitiativeCompleteness(initiative: InitiativeDoc): InitiativeCompletenessResult;
82
+ /**
83
+ * Checks if an initiative has phases defined.
84
+ *
85
+ * Used by wu:create --initiative to warn when linking WUs to
86
+ * initiatives without phases.
87
+ *
88
+ * @param initiative - Initiative document to check
89
+ * @returns Check result with warning if no phases
90
+ *
91
+ * @example
92
+ * const result = checkInitiativePhases(initiative);
93
+ * if (!result.hasPhases) {
94
+ * console.warn(result.warning);
95
+ * }
96
+ */
97
+ export declare function checkInitiativePhases(initiative: InitiativeDoc): InitiativePhaseCheck;
98
+ /**
99
+ * Determines if an initiative status should progress based on WU activity.
100
+ *
101
+ * Auto-progression rules:
102
+ * - draft -> in_progress: when first WU is claimed (status: in_progress)
103
+ * - open -> in_progress: when first WU is claimed (status: in_progress)
104
+ *
105
+ * Terminal statuses (done, archived) never progress.
106
+ *
107
+ * @param initiative - Initiative document to check
108
+ * @param wus - Array of WUs (all WUs, filtering is done internally)
109
+ * @returns Check result with new status if progression is needed
110
+ *
111
+ * @example
112
+ * const result = shouldProgressInitiativeStatus(initiative, allWUs);
113
+ * if (result.shouldProgress) {
114
+ * initiative.status = result.newStatus;
115
+ * // Save initiative
116
+ * }
117
+ */
118
+ export declare function shouldProgressInitiativeStatus(initiative: InitiativeDoc, wus: WUDoc[]): InitiativeProgressCheck;
119
+ /**
120
+ * Finds incomplete initiatives for state:doctor reporting.
121
+ *
122
+ * @param initiatives - Array of initiative documents
123
+ * @returns Array of incomplete initiative reports
124
+ */
125
+ export declare function findIncompleteInitiatives(initiatives: InitiativeDoc[]): Array<{
126
+ id: string;
127
+ warnings: string[];
128
+ }>;
129
+ export {};
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Initiative Completeness Validation (WU-1211)
3
+ *
4
+ * Validates initiative completeness and determines when
5
+ * initiative status should auto-progress.
6
+ *
7
+ * Used by:
8
+ * - initiative:create (warns if description not provided)
9
+ * - wu:create --initiative (warns if initiative has no phases)
10
+ * - wu:claim (auto-progresses initiative status)
11
+ * - state:doctor (reports incomplete initiatives)
12
+ */
13
+ /** Initiative statuses that can transition to in_progress */
14
+ const PROGRESSABLE_STATUSES = ['draft', 'open'];
15
+ /** WU statuses that indicate active work */
16
+ const ACTIVE_WU_STATUSES = ['in_progress'];
17
+ /**
18
+ * Validates initiative completeness and returns warnings for missing fields.
19
+ *
20
+ * This is soft validation - missing fields generate warnings, not errors.
21
+ * The initiative is still considered valid but incomplete.
22
+ *
23
+ * @param initiative - Initiative document to validate
24
+ * @returns Validation result with warnings for incomplete fields
25
+ *
26
+ * @example
27
+ * const result = validateInitiativeCompleteness(initiative);
28
+ * if (result.warnings.length > 0) {
29
+ * console.warn('Initiative is incomplete:', result.warnings);
30
+ * }
31
+ */
32
+ export function validateInitiativeCompleteness(initiative) {
33
+ const warnings = [];
34
+ const id = initiative.id || 'unknown';
35
+ // Check description
36
+ if (!initiative.description || initiative.description.trim() === '') {
37
+ warnings.push(`[${id}] Initiative has no description. Add a description to explain its purpose.`);
38
+ }
39
+ // Check phases
40
+ if (!initiative.phases || initiative.phases.length === 0) {
41
+ warnings.push(`[${id}] Initiative has no phases defined. Add phases to break down the work.`);
42
+ }
43
+ // Check success_metrics
44
+ if (!initiative.success_metrics || initiative.success_metrics.length === 0) {
45
+ warnings.push(`[${id}] Initiative has no success_metrics defined. Add metrics to measure completion.`);
46
+ }
47
+ return {
48
+ valid: true, // Always valid - issues are warnings not errors
49
+ warnings,
50
+ };
51
+ }
52
+ /**
53
+ * Checks if an initiative has phases defined.
54
+ *
55
+ * Used by wu:create --initiative to warn when linking WUs to
56
+ * initiatives without phases.
57
+ *
58
+ * @param initiative - Initiative document to check
59
+ * @returns Check result with warning if no phases
60
+ *
61
+ * @example
62
+ * const result = checkInitiativePhases(initiative);
63
+ * if (!result.hasPhases) {
64
+ * console.warn(result.warning);
65
+ * }
66
+ */
67
+ export function checkInitiativePhases(initiative) {
68
+ const hasPhases = Array.isArray(initiative.phases) && initiative.phases.length > 0;
69
+ const id = initiative.id || 'unknown';
70
+ return {
71
+ hasPhases,
72
+ warning: hasPhases
73
+ ? null
74
+ : `Initiative ${id} has no phases defined. Consider adding phases before linking WUs.`,
75
+ };
76
+ }
77
+ /**
78
+ * Determines if an initiative status should progress based on WU activity.
79
+ *
80
+ * Auto-progression rules:
81
+ * - draft -> in_progress: when first WU is claimed (status: in_progress)
82
+ * - open -> in_progress: when first WU is claimed (status: in_progress)
83
+ *
84
+ * Terminal statuses (done, archived) never progress.
85
+ *
86
+ * @param initiative - Initiative document to check
87
+ * @param wus - Array of WUs (all WUs, filtering is done internally)
88
+ * @returns Check result with new status if progression is needed
89
+ *
90
+ * @example
91
+ * const result = shouldProgressInitiativeStatus(initiative, allWUs);
92
+ * if (result.shouldProgress) {
93
+ * initiative.status = result.newStatus;
94
+ * // Save initiative
95
+ * }
96
+ */
97
+ export function shouldProgressInitiativeStatus(initiative, wus) {
98
+ const currentStatus = initiative.status || 'draft';
99
+ const initiativeId = initiative.id;
100
+ // Terminal statuses cannot progress
101
+ if (!PROGRESSABLE_STATUSES.includes(currentStatus)) {
102
+ return {
103
+ shouldProgress: false,
104
+ newStatus: null,
105
+ };
106
+ }
107
+ // Filter WUs belonging to this initiative
108
+ const initiativeWUs = wus.filter((wu) => wu.initiative === initiativeId);
109
+ // Check if any WU is actively being worked on
110
+ const hasActiveWU = initiativeWUs.some((wu) => ACTIVE_WU_STATUSES.includes(wu.status || ''));
111
+ if (hasActiveWU) {
112
+ return {
113
+ shouldProgress: true,
114
+ newStatus: 'in_progress',
115
+ };
116
+ }
117
+ return {
118
+ shouldProgress: false,
119
+ newStatus: null,
120
+ };
121
+ }
122
+ /**
123
+ * Finds incomplete initiatives for state:doctor reporting.
124
+ *
125
+ * @param initiatives - Array of initiative documents
126
+ * @returns Array of incomplete initiative reports
127
+ */
128
+ export function findIncompleteInitiatives(initiatives) {
129
+ const incompleteList = [];
130
+ for (const init of initiatives) {
131
+ const result = validateInitiativeCompleteness(init);
132
+ if (result.warnings.length > 0) {
133
+ incompleteList.push({
134
+ id: init.id || 'unknown',
135
+ warnings: result.warnings,
136
+ });
137
+ }
138
+ }
139
+ return incompleteList;
140
+ }
@@ -8,8 +8,8 @@
8
8
  * WU-2319: Added bidirectional validation for WU<->Initiative references.
9
9
  * WU-1088: detectCycles moved to @lumenflow/core to break circular dependency.
10
10
  *
11
- * @see {@link tools/validate.mjs} - Consumer (CI validation)
12
- * @see {@link tools/lib/initiative-schema.mjs} - Initiative schema
11
+ * @see {@link packages/@lumenflow/cli/src/validate.ts} - Consumer (CI validation)
12
+ * @see {@link packages/@lumenflow/cli/src/lib/initiative-schema.ts} - Initiative schema
13
13
  */
14
14
  import { detectCycles, type WUObject, type CycleResult } from '@lumenflow/core';
15
15
  export { detectCycles, type WUObject, type CycleResult };
@@ -8,8 +8,8 @@
8
8
  * WU-2319: Added bidirectional validation for WU<->Initiative references.
9
9
  * WU-1088: detectCycles moved to @lumenflow/core to break circular dependency.
10
10
  *
11
- * @see {@link tools/validate.mjs} - Consumer (CI validation)
12
- * @see {@link tools/lib/initiative-schema.mjs} - Initiative schema
11
+ * @see {@link packages/@lumenflow/cli/src/validate.ts} - Consumer (CI validation)
12
+ * @see {@link packages/@lumenflow/cli/src/lib/initiative-schema.ts} - Initiative schema
13
13
  */
14
14
  // WU-1088: Import detectCycles from @lumenflow/core to break circular dependency
15
15
  import { detectCycles } from '@lumenflow/core';
@@ -43,7 +43,7 @@ export interface InitiativeEntry {
43
43
  /**
44
44
  * Initiative YAML I/O module.
45
45
  *
46
- * Mirrors wu-yaml.mjs pattern. Provides validated read/write operations
46
+ * Mirrors wu-yaml.ts pattern. Provides validated read/write operations
47
47
  * for Initiative YAML files.
48
48
  *
49
49
  * @example
@@ -11,7 +11,7 @@ import { WU_PATHS } from '@lumenflow/core/dist/wu-paths.js';
11
11
  /**
12
12
  * Initiative YAML I/O module.
13
13
  *
14
- * Mirrors wu-yaml.mjs pattern. Provides validated read/write operations
14
+ * Mirrors wu-yaml.ts pattern. Provides validated read/write operations
15
15
  * for Initiative YAML files.
16
16
  *
17
17
  * @example
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumenflow/initiatives",
3
- "version": "2.2.2",
3
+ "version": "2.3.2",
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.2.2"
44
+ "@lumenflow/core": "2.3.2"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@vitest/coverage-v8": "^4.0.17",