@lumenflow/cli 2.18.3 → 2.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/README.md +44 -42
  2. package/dist/agent-session.js +1 -1
  3. package/dist/agent-session.js.map +1 -1
  4. package/dist/commands/integrate.js +1 -0
  5. package/dist/commands/integrate.js.map +1 -1
  6. package/dist/commands.js +1 -0
  7. package/dist/commands.js.map +1 -1
  8. package/dist/delegation-list.js +140 -0
  9. package/dist/delegation-list.js.map +1 -0
  10. package/dist/docs-sync.js +1 -0
  11. package/dist/docs-sync.js.map +1 -1
  12. package/dist/doctor.js +36 -99
  13. package/dist/doctor.js.map +1 -1
  14. package/dist/gates-plan-resolvers.js +150 -0
  15. package/dist/gates-plan-resolvers.js.map +1 -0
  16. package/dist/gates-runners.js +533 -0
  17. package/dist/gates-runners.js.map +1 -0
  18. package/dist/gates-types.js +3 -0
  19. package/dist/gates-types.js.map +1 -1
  20. package/dist/gates-utils.js +316 -0
  21. package/dist/gates-utils.js.map +1 -0
  22. package/dist/gates.js +44 -1016
  23. package/dist/gates.js.map +1 -1
  24. package/dist/hooks/enforcement-generator.js +16 -880
  25. package/dist/hooks/enforcement-generator.js.map +1 -1
  26. package/dist/hooks/enforcement-sync.js +6 -5
  27. package/dist/hooks/enforcement-sync.js.map +1 -1
  28. package/dist/hooks/generators/auto-checkpoint.js +123 -0
  29. package/dist/hooks/generators/auto-checkpoint.js.map +1 -0
  30. package/dist/hooks/generators/enforce-worktree.js +188 -0
  31. package/dist/hooks/generators/enforce-worktree.js.map +1 -0
  32. package/dist/hooks/generators/index.js +16 -0
  33. package/dist/hooks/generators/index.js.map +1 -0
  34. package/dist/hooks/generators/pre-compact-checkpoint.js +134 -0
  35. package/dist/hooks/generators/pre-compact-checkpoint.js.map +1 -0
  36. package/dist/hooks/generators/require-wu.js +115 -0
  37. package/dist/hooks/generators/require-wu.js.map +1 -0
  38. package/dist/hooks/generators/session-start-recovery.js +101 -0
  39. package/dist/hooks/generators/session-start-recovery.js.map +1 -0
  40. package/dist/hooks/generators/signal-utils.js +52 -0
  41. package/dist/hooks/generators/signal-utils.js.map +1 -0
  42. package/dist/hooks/generators/warn-incomplete.js +65 -0
  43. package/dist/hooks/generators/warn-incomplete.js.map +1 -0
  44. package/dist/init-detection.js +228 -0
  45. package/dist/init-detection.js.map +1 -0
  46. package/dist/init-scaffolding.js +146 -0
  47. package/dist/init-scaffolding.js.map +1 -0
  48. package/dist/init-templates.js +1928 -0
  49. package/dist/init-templates.js.map +1 -0
  50. package/dist/init.js +137 -2425
  51. package/dist/init.js.map +1 -1
  52. package/dist/initiative-edit.js +42 -11
  53. package/dist/initiative-edit.js.map +1 -1
  54. package/dist/initiative-remove-wu.js +0 -0
  55. package/dist/initiative-status.js +29 -2
  56. package/dist/initiative-status.js.map +1 -1
  57. package/dist/mem-context.js +22 -9
  58. package/dist/mem-context.js.map +1 -1
  59. package/dist/orchestrate-init-status.js +32 -1
  60. package/dist/orchestrate-init-status.js.map +1 -1
  61. package/dist/orchestrate-initiative.js +2 -2
  62. package/dist/orchestrate-initiative.js.map +1 -1
  63. package/dist/orchestrate-monitor.js +38 -38
  64. package/dist/orchestrate-monitor.js.map +1 -1
  65. package/dist/plan-link.js +7 -14
  66. package/dist/plan-link.js.map +1 -1
  67. package/dist/public-manifest.js +19 -5
  68. package/dist/public-manifest.js.map +1 -1
  69. package/dist/shared-validators.js +1 -0
  70. package/dist/shared-validators.js.map +1 -1
  71. package/dist/spawn-list.js +0 -0
  72. package/dist/sync-templates.js +2 -1
  73. package/dist/sync-templates.js.map +1 -1
  74. package/dist/wu-claim-branch.js +121 -0
  75. package/dist/wu-claim-branch.js.map +1 -0
  76. package/dist/wu-claim-output.js +83 -0
  77. package/dist/wu-claim-output.js.map +1 -0
  78. package/dist/wu-claim-resume-handler.js +85 -0
  79. package/dist/wu-claim-resume-handler.js.map +1 -0
  80. package/dist/wu-claim-state.js +572 -0
  81. package/dist/wu-claim-state.js.map +1 -0
  82. package/dist/wu-claim-validation.js +439 -0
  83. package/dist/wu-claim-validation.js.map +1 -0
  84. package/dist/wu-claim-worktree.js +221 -0
  85. package/dist/wu-claim-worktree.js.map +1 -0
  86. package/dist/wu-claim.js +96 -1394
  87. package/dist/wu-claim.js.map +1 -1
  88. package/dist/wu-code-path-coverage.js +81 -0
  89. package/dist/wu-code-path-coverage.js.map +1 -0
  90. package/dist/wu-create-content.js +256 -0
  91. package/dist/wu-create-content.js.map +1 -0
  92. package/dist/wu-create-readiness.js +57 -0
  93. package/dist/wu-create-readiness.js.map +1 -0
  94. package/dist/wu-create-validation.js +124 -0
  95. package/dist/wu-create-validation.js.map +1 -0
  96. package/dist/wu-create.js +45 -442
  97. package/dist/wu-create.js.map +1 -1
  98. package/dist/wu-done.js +151 -249
  99. package/dist/wu-done.js.map +1 -1
  100. package/dist/wu-edit-operations.js +401 -0
  101. package/dist/wu-edit-operations.js.map +1 -0
  102. package/dist/wu-edit-validators.js +280 -0
  103. package/dist/wu-edit-validators.js.map +1 -0
  104. package/dist/wu-edit.js +43 -759
  105. package/dist/wu-edit.js.map +1 -1
  106. package/dist/wu-prep.js +43 -127
  107. package/dist/wu-prep.js.map +1 -1
  108. package/dist/wu-repair.js +1 -1
  109. package/dist/wu-repair.js.map +1 -1
  110. package/dist/wu-sandbox.js +253 -0
  111. package/dist/wu-sandbox.js.map +1 -0
  112. package/dist/wu-spawn-prompt-builders.js +1124 -0
  113. package/dist/wu-spawn-prompt-builders.js.map +1 -0
  114. package/dist/wu-spawn-strategy-resolver.js +319 -0
  115. package/dist/wu-spawn-strategy-resolver.js.map +1 -0
  116. package/dist/wu-spawn.js +9 -1398
  117. package/dist/wu-spawn.js.map +1 -1
  118. package/dist/wu-status.js +4 -0
  119. package/dist/wu-status.js.map +1 -1
  120. package/dist/wu-validate.js +1 -1
  121. package/dist/wu-validate.js.map +1 -1
  122. package/package.json +15 -11
  123. package/templates/core/LUMENFLOW.md.template +29 -99
  124. package/templates/core/UPGRADING.md.template +2 -2
  125. package/templates/core/ai/onboarding/agent-invocation-guide.md.template +1 -1
  126. package/templates/core/ai/onboarding/quick-ref-commands.md.template +29 -4
  127. package/templates/core/ai/onboarding/release-process.md.template +1 -1
  128. package/templates/vendors/claude/.claude/skills/orchestration/SKILL.md.template +8 -8
package/dist/wu-create.js CHANGED
@@ -1,9 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * WU Create Helper (WU-1262, WU-1439)
3
+ * WU Create Orchestrator (WU-1262, WU-1439, WU-1651)
4
4
  *
5
5
  * Race-safe WU creation using shared micro-worktree isolation.
6
6
  *
7
+ * WU-1651: Decomposed into focused modules:
8
+ * - wu-create-validation.ts: Spec validation and strict mode checks
9
+ * - wu-create-content.ts: YAML content building, backlog updates, plan templates
10
+ * - wu-create-readiness.ts: Post-create readiness summary display
11
+ * - wu-create-cloud.ts: Cloud mode context builder
12
+ *
13
+ * This file remains the orchestrator, coordinating CLI parsing, validation,
14
+ * content generation, and transaction execution.
15
+ *
7
16
  * Canonical sequence:
8
17
  * 1) Validate inputs (id, lane, title)
9
18
  * 2) Ensure on main branch
@@ -15,61 +24,37 @@
15
24
  * e) Push to origin/main
16
25
  * f) Cleanup temp branch and micro-worktree
17
26
  *
18
- * Benefits:
19
- * - Main checkout never switches branches (no impact on other agents)
20
- * - Race conditions handled via rebase+retry (up to 3 attempts)
21
- * - Cleanup guaranteed even on failure
22
- *
23
27
  * Usage:
24
28
  * pnpm wu:create --id WU-706 --lane Intelligence --title "Fix XYZ issue"
25
- *
26
- * Context: WU-705 (fix agent coordination failures), WU-1262 (micro-worktree isolation),
27
- * WU-1439 (refactor to shared helper)
28
29
  */
29
30
  import { getGitForCwd } from '@lumenflow/core/git-adapter';
30
31
  import { die } from '@lumenflow/core/error-handler';
31
- import { existsSync, writeFileSync, mkdirSync } from 'node:fs';
32
+ import { existsSync } from 'node:fs';
32
33
  import { join } from 'node:path';
33
- // WU-1352: Use centralized YAML functions from wu-yaml.ts
34
- import { stringifyYAML } from '@lumenflow/core/wu-yaml';
35
- // WU-1428: Use date-utils for consistent YYYY-MM-DD format (library-first)
36
34
  import { todayISO } from '@lumenflow/core/date-utils';
37
35
  import { validateLaneFormat, extractParent } from '@lumenflow/core/lane-checker';
38
- // WU-2330: Import lane inference for sub-lane suggestions
39
36
  import { inferSubLane } from '@lumenflow/core/lane-inference';
40
- import { parseBacklogFrontmatter } from '@lumenflow/core/backlog-parser';
41
37
  import { createWUParser, WU_CREATE_OPTIONS, WU_OPTIONS } from '@lumenflow/core/arg-parser';
42
38
  import { WU_PATHS } from '@lumenflow/core/wu-paths';
43
39
  import { getConfig } from '@lumenflow/core/config';
44
- import { validateWU } from '@lumenflow/core/wu-schema';
45
- import { getPlanPath, getPlanProtocolRef, getPlansDir } from '@lumenflow/core/lumenflow-home';
46
- import { hasSpecRefs, validateSpecRefs } from '@lumenflow/core/wu-create-validators';
47
- import { COMMIT_FORMATS, FILE_SYSTEM, READINESS_UI, REMOTES, STRING_LITERALS, WU_TYPES, } from '@lumenflow/core/wu-constants';
48
- // WU-1593: Use centralized validateWUIDFormat (DRY)
40
+ import { validateSpecRefs, hasSpecRefs } from '@lumenflow/core/wu-create-validators';
41
+ import { COMMIT_FORMATS, REMOTES, STRING_LITERALS, WU_TYPES } from '@lumenflow/core/wu-constants';
49
42
  import { ensureOnMain, validateWUIDFormat } from '@lumenflow/core/wu-helpers';
50
- // WU-1439: Use shared micro-worktree helper
51
43
  import { withMicroWorktree } from '@lumenflow/core/micro-worktree';
52
- // WU-1246: Auto-generate WU IDs when --id not provided
53
44
  import { generateWuIdWithRetry } from '@lumenflow/core/wu-id-generator';
54
- // WU-1620: Import spec completeness validator for readiness summary
55
- import { validateSpecCompleteness } from '@lumenflow/core/wu-done-validators';
56
- // WU-1620: Import readWU to read back created YAML for validation
57
- import { readWU } from '@lumenflow/core/wu-yaml';
58
- // WU-2253: Import WU spec linter for acceptance/code_paths validation
59
45
  import { lintWUSpec, formatLintErrors } from '@lumenflow/core/wu-lint';
60
- // WU-1329: Import path existence validators for strict mode
61
- import { validateCodePathsExistence, validateTestPathsExistence, } from '@lumenflow/core/wu-preflight-validators';
62
- // WU-1025: Import placeholder validator for inline content validation
63
- import { validateNoPlaceholders, buildPlaceholderErrorMessage } from '@lumenflow/core/wu-validator';
64
- import { isCodeFile } from '@lumenflow/core/manual-test-validator';
65
46
  import { WU_CREATE_DEFAULTS } from '@lumenflow/core/wu-create-defaults';
66
- import { isDocsOrProcessType, hasAnyTests, hasManualTests } from '@lumenflow/core/wu-type-helpers';
67
- // WU-1211: Import initiative validation for phase check
47
+ import { isDocsOrProcessType } from '@lumenflow/core/wu-type-helpers';
68
48
  import { checkInitiativePhases, findInitiative } from '@lumenflow/initiatives';
69
- // WU-1590: Cloud create context builder for --cloud path
70
49
  import { buildCloudCreateContext } from './wu-create-cloud.js';
71
- // WU-1495: Cloud auto-detection from config-driven env signals
72
50
  import { detectCloudMode, resolveEffectiveCloudActivation, CLOUD_ACTIVATION_SOURCE, } from '@lumenflow/core/cloud-detect';
51
+ // WU-1651: Import from extracted modules
52
+ import { validateCreateSpec, containsCodeFiles, hasAnyItems, } from './wu-create-validation.js';
53
+ import { buildWUContent, truncateTitle, mergeSpecRefs, createPlanTemplate, createWUYamlInWorktree, updateBacklogInWorktree, getPlanProtocolRef, } from './wu-create-content.js';
54
+ import { displayReadinessSummary } from './wu-create-readiness.js';
55
+ // Re-export public API for backward compatibility (tests import from wu-create.js)
56
+ export { validateCreateSpec } from './wu-create-validation.js';
57
+ export { buildWUContent } from './wu-create-content.js';
73
58
  /** Log prefix for console output */
74
59
  const LOG_PREFIX = '[wu:create]';
75
60
  /** Micro-worktree operation name */
@@ -78,22 +63,8 @@ const OPERATION_NAME = 'wu-create';
78
63
  const DEFAULT_PRIORITY = 'P2';
79
64
  /** Default WU type */
80
65
  const DEFAULT_TYPE = WU_TYPES.FEATURE;
81
- /** Maximum title length before truncation */
82
- const MAX_TITLE_LENGTH = 60;
83
- /** Truncation suffix */
84
- const TRUNCATION_SUFFIX = '...';
85
- /** Truncated title length (MAX_TITLE_LENGTH - TRUNCATION_SUFFIX.length) */
86
- const TRUNCATED_TITLE_LENGTH = MAX_TITLE_LENGTH - TRUNCATION_SUFFIX.length;
87
66
  /** Minimum confidence threshold to show lane suggestion warning (WU-2438: lowered from 50 to 30) */
88
67
  const MIN_CONFIDENCE_FOR_WARNING = 30;
89
- function containsCodeFiles(codePaths) {
90
- if (!codePaths || codePaths.length === 0)
91
- return false;
92
- return codePaths.some((p) => isCodeFile(p));
93
- }
94
- function hasAnyItems(value) {
95
- return Array.isArray(value) && value.length > 0;
96
- }
97
68
  /**
98
69
  * Resolve branch-aware cloud activation for wu:create.
99
70
  *
@@ -185,379 +156,6 @@ function checkWUExists(id) {
185
156
  ` 3. Delete existing WU: pnpm wu:delete --id ${id} (if obsolete)`);
186
157
  }
187
158
  }
188
- /**
189
- * Truncate title for commit message if needed
190
- * @param {string} title - Original title
191
- * @returns {string} Truncated title
192
- */
193
- function truncateTitle(title) {
194
- return title.length > MAX_TITLE_LENGTH
195
- ? `${title.substring(0, TRUNCATED_TITLE_LENGTH)}${TRUNCATION_SUFFIX}`
196
- : title;
197
- }
198
- /**
199
- * WU-1620: Display readiness summary after create/edit
200
- *
201
- * Shows whether WU is ready for wu:claim based on spec completeness.
202
- * Non-blocking - just informational to help agents understand what's missing.
203
- *
204
- * @param {string} id - WU ID
205
- */
206
- function displayReadinessSummary(id) {
207
- try {
208
- const wuPath = WU_PATHS.WU(id);
209
- const wuDoc = readWU(wuPath, id);
210
- const { valid, errors } = validateSpecCompleteness(wuDoc, id);
211
- const { BOX, BOX_WIDTH, MESSAGES, ERROR_MAX_LENGTH, ERROR_TRUNCATE_LENGTH, TRUNCATION_SUFFIX, PADDING, } = READINESS_UI;
212
- console.log(`\n${BOX.TOP_LEFT}${BOX.HORIZONTAL.repeat(BOX_WIDTH)}${BOX.TOP_RIGHT}`);
213
- if (valid) {
214
- console.log(`${BOX.VERTICAL} ${MESSAGES.READY_YES}${''.padEnd(PADDING.READY_YES)}${BOX.VERTICAL}`);
215
- console.log(`${BOX.VERTICAL}${''.padEnd(BOX_WIDTH)}${BOX.VERTICAL}`);
216
- const claimCmd = `Run: pnpm wu:claim --id ${id}`;
217
- console.log(`${BOX.VERTICAL} ${claimCmd}${''.padEnd(BOX_WIDTH - claimCmd.length - 1)}${BOX.VERTICAL}`);
218
- }
219
- else {
220
- console.log(`${BOX.VERTICAL} ${MESSAGES.READY_NO}${''.padEnd(PADDING.READY_NO)}${BOX.VERTICAL}`);
221
- console.log(`${BOX.VERTICAL}${''.padEnd(BOX_WIDTH)}${BOX.VERTICAL}`);
222
- console.log(`${BOX.VERTICAL} ${MESSAGES.MISSING_HEADER}${''.padEnd(PADDING.MISSING_HEADER)}${BOX.VERTICAL}`);
223
- for (const error of errors) {
224
- // Truncate long error messages to fit box
225
- const truncated = error.length > ERROR_MAX_LENGTH
226
- ? `${error.substring(0, ERROR_TRUNCATE_LENGTH)}${TRUNCATION_SUFFIX}`
227
- : error;
228
- console.log(`${BOX.VERTICAL} ${MESSAGES.BULLET} ${truncated}${''.padEnd(Math.max(0, PADDING.ERROR_BULLET - truncated.length))}${BOX.VERTICAL}`);
229
- }
230
- console.log(`${BOX.VERTICAL}${''.padEnd(BOX_WIDTH)}${BOX.VERTICAL}`);
231
- const editCmd = `Run: pnpm wu:edit --id ${id} --help`;
232
- console.log(`${BOX.VERTICAL} ${editCmd}${''.padEnd(BOX_WIDTH - editCmd.length - 1)}${BOX.VERTICAL}`);
233
- }
234
- console.log(`${BOX.BOTTOM_LEFT}${BOX.HORIZONTAL.repeat(BOX_WIDTH)}${BOX.BOTTOM_RIGHT}`);
235
- }
236
- catch (err) {
237
- // Non-blocking - if validation fails, just warn
238
- console.warn(`${LOG_PREFIX} ⚠️ Could not validate readiness: ${err.message}`);
239
- }
240
- }
241
- function mergeSpecRefs(specRefs, extraRef) {
242
- const refs = specRefs ? [...specRefs] : [];
243
- if (extraRef && !refs.includes(extraRef)) {
244
- refs.push(extraRef);
245
- }
246
- return refs;
247
- }
248
- function createPlanTemplate(wuId, title) {
249
- const plansDir = getPlansDir();
250
- mkdirSync(plansDir, { recursive: true });
251
- const planPath = getPlanPath(wuId);
252
- if (existsSync(planPath)) {
253
- die(`Plan already exists: ${planPath}\n\n` +
254
- `Options:\n` +
255
- ` 1. Open the existing plan and continue editing\n` +
256
- ` 2. Delete or rename the existing plan before retrying\n` +
257
- ` 3. Run wu:create without --plan`);
258
- }
259
- const today = todayISO();
260
- const content = `# ${wuId} Plan — ${title}\n\n` +
261
- `Created: ${today}\n\n` +
262
- `## Goal\n\n` +
263
- `## Scope\n\n` +
264
- `## Approach\n\n` +
265
- `## Risks\n\n` +
266
- `## Open Questions\n`;
267
- writeFileSync(planPath, content, { encoding: FILE_SYSTEM.UTF8 });
268
- console.log(`${LOG_PREFIX} ✅ Created plan template: ${planPath}`);
269
- return planPath;
270
- }
271
- export function buildWUContent({ id, lane, title, priority, type, created, opts, }) {
272
- const { description, acceptance, notes, codePaths, testPathsManual, testPathsUnit, testPathsE2e, initiative, phase, blockedBy, blocks, labels, assignedTo, exposure, userJourney, uiPairingWus, specRefs, } = opts;
273
- // Arrays come directly from Commander.js repeatable options - no parsing needed
274
- const code_paths = codePaths ?? [];
275
- const tests = {
276
- manual: testPathsManual ?? [],
277
- unit: testPathsUnit ?? [],
278
- e2e: testPathsE2e ?? [],
279
- };
280
- // WU-1443: Auto-insert minimal manual test stub for plan-first specs when no tests are provided,
281
- // as long as code_paths does not include actual code files (automated tests still required for code).
282
- if (!isDocsOrProcessType(type) && !hasAnyTests(tests) && !containsCodeFiles(code_paths)) {
283
- tests.manual = [WU_CREATE_DEFAULTS.AUTO_MANUAL_TEST_PLACEHOLDER];
284
- }
285
- return {
286
- id,
287
- title,
288
- lane,
289
- type,
290
- status: 'ready',
291
- priority,
292
- created,
293
- description,
294
- acceptance,
295
- code_paths,
296
- tests,
297
- artifacts: [WU_PATHS.STAMP(id)],
298
- dependencies: [],
299
- risks: [],
300
- // WU-1443: Default notes to non-empty placeholder to avoid strict completeness failures.
301
- notes: typeof notes === 'string' && notes.trim().length > 0
302
- ? notes
303
- : WU_CREATE_DEFAULTS.AUTO_NOTES_PLACEHOLDER,
304
- requires_review: false,
305
- ...(initiative && { initiative }),
306
- ...(phase && { phase: parseInt(phase, 10) }),
307
- ...(blockedBy?.length && { blocked_by: blockedBy }),
308
- ...(blocks?.length && { blocks }),
309
- ...(labels?.length && { labels }),
310
- ...(assignedTo && { assigned_to: assignedTo }),
311
- ...(exposure && { exposure }),
312
- ...(userJourney && { user_journey: userJourney }),
313
- ...(uiPairingWus?.length && { ui_pairing_wus: uiPairingWus }),
314
- ...(specRefs?.length && { spec_refs: specRefs }),
315
- };
316
- }
317
- /**
318
- * Validate WU spec for creation
319
- *
320
- * WU-1329: Strict mode (default) validates that code_paths and test_paths exist on disk.
321
- * Use opts.strict = false to bypass path existence checks.
322
- *
323
- * @param params - Validation parameters
324
- * @returns {{ valid: boolean, errors: string[] }}
325
- */
326
- export function validateCreateSpec({ id, lane, title, priority, type, opts, }) {
327
- const errors = [];
328
- const effectiveType = type || DEFAULT_TYPE;
329
- // WU-1329: Strict mode is the default
330
- const strict = opts.strict !== false;
331
- // WU-1329: Log when strict validation is bypassed
332
- if (!strict) {
333
- console.warn(`${LOG_PREFIX} WARNING: strict validation bypassed (--no-strict). Path existence checks skipped.`);
334
- }
335
- if (!opts.description) {
336
- errors.push('--description is required');
337
- }
338
- if (!opts.acceptance || opts.acceptance.length === 0) {
339
- errors.push('--acceptance is required (repeatable)');
340
- }
341
- if (!opts.exposure) {
342
- errors.push('--exposure is required');
343
- }
344
- const hasTestPaths = hasAnyItems(opts.testPathsManual) ||
345
- hasAnyItems(opts.testPathsUnit) ||
346
- hasAnyItems(opts.testPathsE2e);
347
- const hasManualTestPaths = hasManualTests({ manual: opts.testPathsManual });
348
- if (!isDocsOrProcessType(effectiveType)) {
349
- const codePaths = opts.codePaths ?? [];
350
- if (codePaths.length === 0) {
351
- errors.push('--code-paths is required for non-documentation WUs');
352
- }
353
- // WU-1443: Plan-first WUs may not know tests yet.
354
- // Allow auto-manual stub ONLY when code_paths does not include code files.
355
- const canAutoAddManualTests = !hasTestPaths && codePaths.length > 0 && !containsCodeFiles(codePaths);
356
- if (!hasTestPaths && !canAutoAddManualTests) {
357
- errors.push('At least one test path flag is required (--test-paths-manual, --test-paths-unit, or --test-paths-e2e)');
358
- }
359
- if (!hasManualTestPaths && !canAutoAddManualTests) {
360
- errors.push('--test-paths-manual is required for non-documentation WUs');
361
- }
362
- }
363
- if (effectiveType === WU_TYPES.FEATURE && !hasSpecRefs(opts.specRefs)) {
364
- errors.push('--spec-refs is required for type: feature WUs\n' +
365
- ' Tip: Create a plan first with: pnpm plan:create --id <WU-ID> --title "..."\n' +
366
- ' Then use --plan flag or --spec-refs lumenflow://plans/<WU-ID>-plan.md');
367
- }
368
- // WU-1530: Single-pass validation — collect all errors before returning.
369
- // Always build WU content and run all validation stages, even when early fields are missing.
370
- // buildWUContent handles undefined gracefully; Zod catches missing required fields.
371
- // Stage 2b: Placeholder check (only meaningful if fields exist)
372
- if (opts.description && opts.acceptance && opts.acceptance.length > 0) {
373
- const placeholderResult = validateNoPlaceholders({
374
- description: opts.description,
375
- acceptance: opts.acceptance,
376
- });
377
- if (!placeholderResult.valid) {
378
- errors.push(buildPlaceholderErrorMessage('wu:create', placeholderResult));
379
- }
380
- }
381
- // Stage 2c-2d: Schema + completeness — always run to catch enum/format errors
382
- // even when required fields are missing (Zod reports both)
383
- const today = todayISO();
384
- const wuContent = buildWUContent({
385
- id,
386
- lane,
387
- title,
388
- priority,
389
- type: effectiveType,
390
- created: today,
391
- opts,
392
- });
393
- const schemaResult = validateWU(wuContent);
394
- if (!schemaResult.success) {
395
- // Deduplicate: skip schema errors already covered by field-level checks above
396
- const fieldErrorFields = new Set(['description', 'acceptance', 'code_paths', 'tests']);
397
- const schemaErrors = schemaResult.error.issues
398
- .filter((issue) => !fieldErrorFields.has(issue.path[0]) || errors.length === 0)
399
- .map((issue) => `${issue.path.join('.')}: ${issue.message}`);
400
- errors.push(...schemaErrors);
401
- }
402
- // Only run completeness if schema passed (it depends on well-formed data)
403
- if (schemaResult.success) {
404
- const completeness = validateSpecCompleteness(wuContent, id);
405
- if (!completeness.valid) {
406
- errors.push(...completeness.errors);
407
- }
408
- }
409
- // Stage 2e: Strict mode validates path existence
410
- if (strict) {
411
- const rootDir = process.cwd();
412
- if (opts.codePaths && opts.codePaths.length > 0) {
413
- const codePathsResult = validateCodePathsExistence(opts.codePaths, rootDir);
414
- if (!codePathsResult.valid) {
415
- errors.push(...codePathsResult.errors);
416
- }
417
- }
418
- const testsObj = {
419
- unit: opts.testPathsUnit || [],
420
- e2e: opts.testPathsE2e || [],
421
- };
422
- const testPathsResult = validateTestPathsExistence(testsObj, rootDir);
423
- if (!testPathsResult.valid) {
424
- errors.push(...testPathsResult.errors);
425
- }
426
- }
427
- if (errors.length > 0) {
428
- return { valid: false, errors };
429
- }
430
- return { valid: true, errors: [] };
431
- }
432
- /**
433
- * Create WU YAML file in micro-worktree
434
- *
435
- * @param {string} worktreePath - Path to micro-worktree
436
- * @param {string} id - WU ID
437
- * @param {string} lane - WU lane
438
- * @param {string} title - WU title
439
- * @param {string} priority - WU priority
440
- * @param {string} type - WU type
441
- * @param {Object} opts - Additional options
442
- * @returns {string} Relative path to created YAML file
443
- */
444
- function createWUYamlInWorktree(worktreePath, id, lane, title, priority, type, opts = {}) {
445
- const wuRelativePath = WU_PATHS.WU(id);
446
- const wuAbsolutePath = join(worktreePath, wuRelativePath);
447
- const wuDir = join(worktreePath, WU_PATHS.WU_DIR());
448
- mkdirSync(wuDir, { recursive: true });
449
- // WU-1428: Use todayISO() for consistent YYYY-MM-DD format (library-first)
450
- const today = todayISO();
451
- const wuContent = buildWUContent({
452
- id,
453
- lane,
454
- title,
455
- priority,
456
- type,
457
- created: today,
458
- opts,
459
- });
460
- // WU-1539: Validate WU structure before writing (fail-fast, no placeholders)
461
- // WU-1750: Zod transforms normalize embedded newlines in arrays and strings
462
- const validationResult = validateWU(wuContent);
463
- if (!validationResult.success) {
464
- const errors = validationResult.error.issues
465
- .map((issue) => ` • ${issue.path.join('.')}: ${issue.message}`)
466
- .join(STRING_LITERALS.NEWLINE);
467
- die(`${LOG_PREFIX} ❌ WU YAML validation failed:\n\n${errors}\n\n` +
468
- `Fix the issues above and retry.`);
469
- }
470
- const completenessResult = validateSpecCompleteness(wuContent, id);
471
- if (!completenessResult.valid) {
472
- const errorList = completenessResult.errors
473
- .map((error) => ` • ${error}`)
474
- .join(STRING_LITERALS.NEWLINE);
475
- die(`${LOG_PREFIX} ❌ WU SPEC INCOMPLETE:\n\n${errorList}\n\n` +
476
- `Provide the missing fields and retry.`);
477
- }
478
- // WU-2253: Validate acceptance/code_paths consistency and invariants compliance
479
- // This blocks WU creation if acceptance references paths not in code_paths
480
- // or if code_paths conflicts with tools/invariants.yml
481
- const invariantsPath = join(process.cwd(), 'tools/invariants.yml');
482
- const lintResult = lintWUSpec(wuContent, { invariantsPath });
483
- if (!lintResult.valid) {
484
- const formatted = formatLintErrors(lintResult.errors);
485
- die(`${LOG_PREFIX} ❌ WU SPEC LINT FAILED:\n\n${formatted}\n` +
486
- `Fix the issues above before creating this WU.`);
487
- }
488
- // WU-1352: Use centralized stringify (lineWidth: -1 = no wrapping for WU creation)
489
- // WU-1750: CRITICAL - Use validationResult.data (transformed) NOT wuContent (raw input)
490
- // This ensures embedded newlines are normalized before YAML output
491
- const yamlContent = stringifyYAML(validationResult.data, { lineWidth: -1 });
492
- writeFileSync(wuAbsolutePath, yamlContent, { encoding: FILE_SYSTEM.UTF8 });
493
- console.log(`${LOG_PREFIX} ✅ Created ${id}.yaml in micro-worktree`);
494
- return wuRelativePath;
495
- }
496
- /**
497
- * Update backlog.md in micro-worktree
498
- *
499
- * @param {string} worktreePath - Path to micro-worktree
500
- * @param {string} id - WU ID
501
- * @param {string} lane - WU lane
502
- * @param {string} title - WU title
503
- * @returns {string} Relative path to backlog.md
504
- */
505
- function updateBacklogInWorktree(worktreePath, id, lane, title) {
506
- const backlogRelativePath = WU_PATHS.BACKLOG();
507
- const backlogAbsolutePath = join(worktreePath, backlogRelativePath);
508
- if (!existsSync(backlogAbsolutePath)) {
509
- // WU-1311: Use config-based backlog path in error message
510
- die(`Backlog not found in micro-worktree: ${backlogAbsolutePath}\n\n` +
511
- `Options:\n` +
512
- ` 1. Ensure backlog.md exists at ${getConfig().directories.backlogPath}\n` +
513
- ` 2. Run from repository root directory`);
514
- }
515
- const { frontmatter, markdown } = parseBacklogFrontmatter(backlogAbsolutePath);
516
- if (!frontmatter) {
517
- die('Backlog frontmatter missing in micro-worktree.\n\n' +
518
- 'The backlog.md file requires YAML frontmatter to define section headings.\n\n' +
519
- 'Options:\n' +
520
- ' 1. Check backlog.md has valid YAML frontmatter between --- markers\n' +
521
- ' 2. Ensure sections.ready.heading is defined in frontmatter');
522
- }
523
- if (!frontmatter.sections?.ready?.heading) {
524
- die('Invalid backlog frontmatter: Missing sections.ready.heading\n\n' +
525
- 'Options:\n' +
526
- ' 1. Add sections.ready.heading to backlog.md frontmatter\n' +
527
- ' 2. Check frontmatter YAML structure');
528
- }
529
- const readyHeading = frontmatter.sections.ready.heading;
530
- const insertionStrategy = frontmatter.sections.ready.insertion || 'after_heading_blank_line';
531
- const lines = markdown.split(STRING_LITERALS.NEWLINE);
532
- const headingIndex = lines.findIndex((line) => line === readyHeading);
533
- if (headingIndex === -1) {
534
- die(`Could not find Ready section heading: '${readyHeading}'\n\n` +
535
- `Options:\n` +
536
- ` 1. Add the heading '${readyHeading}' to backlog.md\n` +
537
- ` 2. Update sections.ready.heading in backlog.md frontmatter`);
538
- }
539
- let insertionIndex;
540
- if (insertionStrategy === 'after_heading_blank_line') {
541
- const LINES_AFTER_HEADING = 2;
542
- insertionIndex = headingIndex + LINES_AFTER_HEADING;
543
- }
544
- else {
545
- die(`Unknown insertion strategy: ${insertionStrategy}\n\n` +
546
- `Options:\n` +
547
- ` 1. Use 'after_heading_blank_line' in backlog.md frontmatter\n` +
548
- ` 2. Check sections.ready.insertion value`);
549
- }
550
- const newEntry = `- [${id} — ${title}](wu/${id}.yaml) — ${lane}`;
551
- lines.splice(insertionIndex, 0, newEntry);
552
- const updatedMarkdown = lines.join(STRING_LITERALS.NEWLINE);
553
- // WU-1352: Use centralized stringify for frontmatter
554
- const updatedBacklog = `---\n${stringifyYAML(frontmatter, { lineWidth: -1 })}---\n${updatedMarkdown}`;
555
- writeFileSync(backlogAbsolutePath, updatedBacklog, {
556
- encoding: FILE_SYSTEM.UTF8,
557
- });
558
- console.log(`${LOG_PREFIX} ✅ Updated backlog.md in micro-worktree`);
559
- return backlogRelativePath;
560
- }
561
159
  /**
562
160
  * Get default assigned_to value from git config user.email (WU-1368)
563
161
  * @returns {Promise<string>} User email or empty string if not configured
@@ -568,7 +166,7 @@ async function getDefaultAssignedTo() {
568
166
  return email || '';
569
167
  }
570
168
  catch {
571
- console.warn(`${LOG_PREFIX} ⚠️ git config user.email not set - assigned_to will be empty`);
169
+ console.warn(`${LOG_PREFIX} git config user.email not set - assigned_to will be empty`);
572
170
  return '';
573
171
  }
574
172
  }
@@ -690,17 +288,19 @@ async function main() {
690
288
  // WU-1368: Get assigned_to from flag or git config user.email
691
289
  const assignedTo = args.assignedTo || (await getDefaultAssignedTo());
692
290
  if (!assignedTo) {
693
- console.warn(`${LOG_PREFIX} ⚠️ No assigned_to set - WU will need manual assignment`);
291
+ console.warn(`${LOG_PREFIX} No assigned_to set - WU will need manual assignment`);
694
292
  }
695
293
  const planSpecRef = args.plan ? getPlanProtocolRef(wuId) : undefined;
696
- const mergedSpecRefs = mergeSpecRefs(args.specRefs, planSpecRef);
294
+ const mergedRefs = mergeSpecRefs(args.specRefs, planSpecRef);
295
+ // WU-1683: Set first-class plan field (symmetric with initiative related_plan)
296
+ const resolvedPlan = args.plan ? getPlanProtocolRef(wuId) : undefined;
697
297
  // WU-1443: Apply resilient defaults so a plan-first WU doesn't immediately fail strict validation.
698
298
  const effectiveType = args.type || DEFAULT_TYPE;
699
299
  const resolvedNotes = typeof args.notes === 'string' && args.notes.trim().length > 0
700
300
  ? args.notes
701
301
  : WU_CREATE_DEFAULTS.AUTO_NOTES_PLACEHOLDER;
702
302
  if (resolvedNotes === WU_CREATE_DEFAULTS.AUTO_NOTES_PLACEHOLDER) {
703
- console.warn(`${LOG_PREFIX} ⚠️ No --notes provided; using placeholder notes (edit before done).`);
303
+ console.warn(`${LOG_PREFIX} No --notes provided; using placeholder notes (edit before done).`);
704
304
  }
705
305
  const hasProvidedTests = hasAnyItems(args.testPathsManual) ||
706
306
  hasAnyItems(args.testPathsUnit) ||
@@ -710,7 +310,7 @@ async function main() {
710
310
  ? [WU_CREATE_DEFAULTS.AUTO_MANUAL_TEST_PLACEHOLDER]
711
311
  : args.testPathsManual;
712
312
  if (canAutoAddManualTests) {
713
- console.warn(`${LOG_PREFIX} ⚠️ No test paths provided; inserting a minimal manual test stub (add automated tests before code changes).`);
313
+ console.warn(`${LOG_PREFIX} No test paths provided; inserting a minimal manual test stub (add automated tests before code changes).`);
714
314
  }
715
315
  const createSpecValidation = validateCreateSpec({
716
316
  id: wuId,
@@ -729,7 +329,7 @@ async function main() {
729
329
  exposure: args.exposure,
730
330
  userJourney: args.userJourney,
731
331
  uiPairingWus: args.uiPairingWus,
732
- specRefs: mergedSpecRefs,
332
+ specRefs: mergedRefs,
733
333
  initiative: args.initiative,
734
334
  phase: args.phase,
735
335
  blockedBy: args.blockedBy,
@@ -742,11 +342,11 @@ async function main() {
742
342
  });
743
343
  if (!createSpecValidation.valid) {
744
344
  const errorList = createSpecValidation.errors
745
- .map((error) => ` ${error}`)
345
+ .map((error) => ` - ${error}`)
746
346
  .join(STRING_LITERALS.NEWLINE);
747
- die(`${LOG_PREFIX} Spec validation failed:\n\n${errorList}`);
347
+ die(`${LOG_PREFIX} Spec validation failed:\n\n${errorList}`);
748
348
  }
749
- console.log(`${LOG_PREFIX} Spec validation passed`);
349
+ console.log(`${LOG_PREFIX} Spec validation passed`);
750
350
  // WU-1530: Run spec lint BEFORE micro-worktree creation.
751
351
  // Previously this ran inside createWUYamlInWorktree after worktree setup,
752
352
  // meaning lint errors only appeared after a ~10s worktree creation.
@@ -768,7 +368,8 @@ async function main() {
768
368
  exposure: args.exposure,
769
369
  userJourney: args.userJourney,
770
370
  uiPairingWus: args.uiPairingWus,
771
- specRefs: mergedSpecRefs,
371
+ specRefs: mergedRefs,
372
+ plan: resolvedPlan,
772
373
  initiative: args.initiative,
773
374
  phase: args.phase,
774
375
  blockedBy: args.blockedBy,
@@ -778,23 +379,23 @@ async function main() {
778
379
  },
779
380
  });
780
381
  const invariantsPath = join(process.cwd(), 'tools/invariants.yml');
781
- const preflightLint = lintWUSpec(preflightWU, { invariantsPath });
382
+ const preflightLint = lintWUSpec(preflightWU, { invariantsPath, phase: 'intent' });
782
383
  if (!preflightLint.valid) {
783
384
  const formatted = formatLintErrors(preflightLint.errors);
784
- die(`${LOG_PREFIX} WU SPEC LINT FAILED:\n\n${formatted}\n` +
385
+ die(`${LOG_PREFIX} WU SPEC LINT FAILED:\n\n${formatted}\n` +
785
386
  `Fix the issues above before creating this WU.`);
786
387
  }
787
- const specRefsList = mergedSpecRefs;
388
+ const specRefsList = mergedRefs;
788
389
  const specRefsValidation = validateSpecRefs(specRefsList);
789
390
  if (!specRefsValidation.valid) {
790
391
  const errorList = specRefsValidation.errors
791
- .map((error) => ` ${error}`)
392
+ .map((error) => ` - ${error}`)
792
393
  .join(STRING_LITERALS.NEWLINE);
793
- die(`${LOG_PREFIX} Spec reference validation failed:\n\n${errorList}`);
394
+ die(`${LOG_PREFIX} Spec reference validation failed:\n\n${errorList}`);
794
395
  }
795
396
  if (specRefsValidation.warnings.length > 0) {
796
397
  for (const warning of specRefsValidation.warnings) {
797
- console.warn(`${LOG_PREFIX} ⚠️ ${warning}`);
398
+ console.warn(`${LOG_PREFIX} ${warning}`);
798
399
  }
799
400
  }
800
401
  if (args.initiative) {
@@ -807,7 +408,7 @@ async function main() {
807
408
  specRefs: specRefsList,
808
409
  });
809
410
  for (const warning of warnings) {
810
- console.warn(`${LOG_PREFIX} ⚠️ ${warning}`);
411
+ console.warn(`${LOG_PREFIX} ${warning}`);
811
412
  }
812
413
  }
813
414
  }
@@ -841,7 +442,9 @@ async function main() {
841
442
  userJourney: args.userJourney,
842
443
  uiPairingWus: args.uiPairingWus,
843
444
  // WU-2320: Spec references
844
- specRefs: mergedSpecRefs,
445
+ specRefs: mergedRefs,
446
+ // WU-1683: First-class plan field
447
+ plan: resolvedPlan,
845
448
  };
846
449
  if (cloudCtx.skipMicroWorktree) {
847
450
  // WU-1590: Cloud path - write and commit directly on current branch
@@ -893,7 +496,7 @@ async function main() {
893
496
  }
894
497
  }
895
498
  }
896
- console.log(`\n${LOG_PREFIX} Transaction complete!`);
499
+ console.log(`\n${LOG_PREFIX} Transaction complete!`);
897
500
  console.log(`\nWU ${wuId} created successfully:`);
898
501
  console.log(` File: ${WU_PATHS.WU(wuId)}`);
899
502
  console.log(` Lane: ${args.lane}`);