@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 +1 -0
- package/dist/index.js +1 -0
- package/dist/initiative-constants.d.ts +1 -1
- package/dist/initiative-constants.js +1 -1
- package/dist/initiative-orchestrator.d.ts +74 -5
- package/dist/initiative-orchestrator.js +414 -25
- package/dist/initiative-paths.d.ts +1 -1
- package/dist/initiative-paths.js +1 -1
- package/dist/initiative-schema.d.ts +2 -2
- package/dist/initiative-schema.js +2 -2
- package/dist/initiative-validation.d.ts +129 -0
- package/dist/initiative-validation.js +140 -0
- package/dist/initiative-validator.d.ts +2 -2
- package/dist/initiative-validator.js +2 -2
- package/dist/initiative-yaml.d.ts +1 -1
- package/dist/initiative-yaml.js +1 -1
- package/package.json +2 -2
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.
|
|
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.
|
|
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
|
|
19
|
-
* @see {@link
|
|
20
|
-
* @see {@link
|
|
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
|
|
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
|
|
19
|
-
* @see {@link
|
|
20
|
-
* @see {@link
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
757
|
-
|
|
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.
|
|
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
|
-
//
|
|
772
|
-
|
|
773
|
-
|
|
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:
|
|
1204
|
+
status: MANIFEST_WU_STATUS,
|
|
816
1205
|
})),
|
|
817
1206
|
lane_validation: 'pass',
|
|
818
1207
|
done_criteria: 'All stamps exist in .lumenflow/stamps/',
|
package/dist/initiative-paths.js
CHANGED
|
@@ -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.
|
|
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
|
|
8
|
-
* @see {@link
|
|
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
|
|
8
|
-
* @see {@link
|
|
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
|
|
12
|
-
* @see {@link
|
|
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
|
|
12
|
-
* @see {@link
|
|
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.
|
|
46
|
+
* Mirrors wu-yaml.ts pattern. Provides validated read/write operations
|
|
47
47
|
* for Initiative YAML files.
|
|
48
48
|
*
|
|
49
49
|
* @example
|
package/dist/initiative-yaml.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
44
|
+
"@lumenflow/core": "2.3.2"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@vitest/coverage-v8": "^4.0.17",
|