@lumenflow/cli 2.4.0 → 2.5.1

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 (147) hide show
  1. package/README.md +11 -8
  2. package/dist/__tests__/init-config-lanes.test.js +131 -0
  3. package/dist/__tests__/init-docs-structure.test.js +119 -0
  4. package/dist/__tests__/init-lane-inference.test.js +125 -0
  5. package/dist/__tests__/init-onboarding-docs.test.js +132 -0
  6. package/dist/__tests__/init-quick-ref.test.js +145 -0
  7. package/dist/__tests__/init-scripts.test.js +207 -0
  8. package/dist/__tests__/init-template-portability.test.js +97 -0
  9. package/dist/__tests__/init.test.js +7 -2
  10. package/dist/__tests__/initiative-add-wu.test.js +420 -0
  11. package/dist/__tests__/initiative-plan-replacement.test.js +162 -0
  12. package/dist/__tests__/initiative-remove-wu.test.js +458 -0
  13. package/dist/__tests__/onboarding-smoke-test.test.js +211 -0
  14. package/dist/__tests__/path-centralization-cli.test.js +234 -0
  15. package/dist/__tests__/plan-create.test.js +126 -0
  16. package/dist/__tests__/plan-edit.test.js +157 -0
  17. package/dist/__tests__/plan-link.test.js +239 -0
  18. package/dist/__tests__/plan-promote.test.js +181 -0
  19. package/dist/__tests__/templates-sync.test.js +219 -0
  20. package/dist/__tests__/wu-create-strict.test.js +118 -0
  21. package/dist/__tests__/wu-edit-strict.test.js +109 -0
  22. package/dist/__tests__/wu-validate-strict.test.js +113 -0
  23. package/dist/flow-bottlenecks.js +4 -2
  24. package/dist/gates.js +22 -0
  25. package/dist/init.js +670 -87
  26. package/dist/initiative-add-wu.js +112 -16
  27. package/dist/initiative-remove-wu.js +248 -0
  28. package/dist/onboarding-smoke-test.js +400 -0
  29. package/dist/orchestrate-init-status.js +37 -9
  30. package/dist/orchestrate-initiative.js +10 -4
  31. package/dist/plan-create.js +199 -0
  32. package/dist/plan-edit.js +235 -0
  33. package/dist/plan-link.js +233 -0
  34. package/dist/plan-promote.js +231 -0
  35. package/dist/sync-templates.js +137 -5
  36. package/dist/wu-block.js +16 -5
  37. package/dist/wu-claim.js +15 -9
  38. package/dist/wu-create.js +50 -2
  39. package/dist/wu-deps.js +3 -1
  40. package/dist/wu-done.js +14 -5
  41. package/dist/wu-edit.js +35 -0
  42. package/dist/wu-prep.js +131 -8
  43. package/dist/wu-spawn.js +14 -1
  44. package/dist/wu-unblock.js +34 -2
  45. package/dist/wu-validate.js +25 -17
  46. package/package.json +11 -7
  47. package/templates/core/.lumenflow/constraints.md.template +61 -3
  48. package/templates/core/AGENTS.md.template +2 -2
  49. package/templates/core/LUMENFLOW.md.template +85 -23
  50. package/templates/core/ai/onboarding/agent-invocation-guide.md.template +157 -0
  51. package/templates/core/ai/onboarding/agent-safety-card.md.template +227 -0
  52. package/templates/core/ai/onboarding/docs-generation.md.template +277 -0
  53. package/templates/core/ai/onboarding/first-wu-mistakes.md.template +49 -7
  54. package/templates/core/ai/onboarding/quick-ref-commands.md.template +343 -110
  55. package/templates/core/ai/onboarding/release-process.md.template +8 -2
  56. package/templates/core/ai/onboarding/starting-prompt.md.template +407 -0
  57. package/templates/core/ai/onboarding/test-ratchet.md.template +131 -0
  58. package/templates/core/ai/onboarding/troubleshooting-wu-done.md.template +91 -38
  59. package/templates/core/ai/onboarding/vendor-support.md.template +219 -0
  60. package/templates/vendors/claude/.claude/skills/context-management/SKILL.md.template +13 -1
  61. package/templates/vendors/claude/.claude/skills/execution-memory/SKILL.md.template +14 -16
  62. package/templates/vendors/claude/.claude/skills/orchestration/SKILL.md.template +48 -4
  63. package/templates/vendors/claude/.claude/skills/worktree-discipline/SKILL.md.template +5 -1
  64. package/templates/vendors/claude/.claude/skills/wu-lifecycle/SKILL.md.template +19 -8
  65. package/dist/__tests__/init-plan.test.js +0 -340
  66. package/dist/agent-issues-query.d.ts +0 -16
  67. package/dist/agent-log-issue.d.ts +0 -10
  68. package/dist/agent-session-end.d.ts +0 -10
  69. package/dist/agent-session.d.ts +0 -10
  70. package/dist/backlog-prune.d.ts +0 -84
  71. package/dist/cli-entry-point.d.ts +0 -8
  72. package/dist/deps-add.d.ts +0 -91
  73. package/dist/deps-remove.d.ts +0 -17
  74. package/dist/docs-sync.d.ts +0 -50
  75. package/dist/file-delete.d.ts +0 -84
  76. package/dist/file-edit.d.ts +0 -82
  77. package/dist/file-read.d.ts +0 -92
  78. package/dist/file-write.d.ts +0 -90
  79. package/dist/flow-bottlenecks.d.ts +0 -16
  80. package/dist/flow-report.d.ts +0 -16
  81. package/dist/gates.d.ts +0 -94
  82. package/dist/git-branch.d.ts +0 -65
  83. package/dist/git-diff.d.ts +0 -58
  84. package/dist/git-log.d.ts +0 -69
  85. package/dist/git-status.d.ts +0 -58
  86. package/dist/guard-locked.d.ts +0 -62
  87. package/dist/guard-main-branch.d.ts +0 -50
  88. package/dist/guard-worktree-commit.d.ts +0 -59
  89. package/dist/index.d.ts +0 -10
  90. package/dist/init-plan.d.ts +0 -80
  91. package/dist/init-plan.js +0 -337
  92. package/dist/init.d.ts +0 -46
  93. package/dist/initiative-add-wu.d.ts +0 -22
  94. package/dist/initiative-bulk-assign-wus.d.ts +0 -16
  95. package/dist/initiative-create.d.ts +0 -28
  96. package/dist/initiative-edit.d.ts +0 -34
  97. package/dist/initiative-list.d.ts +0 -12
  98. package/dist/initiative-status.d.ts +0 -11
  99. package/dist/lumenflow-upgrade.d.ts +0 -103
  100. package/dist/mem-checkpoint.d.ts +0 -16
  101. package/dist/mem-cleanup.d.ts +0 -29
  102. package/dist/mem-create.d.ts +0 -17
  103. package/dist/mem-export.d.ts +0 -10
  104. package/dist/mem-inbox.d.ts +0 -35
  105. package/dist/mem-init.d.ts +0 -15
  106. package/dist/mem-ready.d.ts +0 -16
  107. package/dist/mem-signal.d.ts +0 -16
  108. package/dist/mem-start.d.ts +0 -16
  109. package/dist/mem-summarize.d.ts +0 -22
  110. package/dist/mem-triage.d.ts +0 -22
  111. package/dist/metrics-cli.d.ts +0 -90
  112. package/dist/metrics-snapshot.d.ts +0 -18
  113. package/dist/orchestrate-init-status.d.ts +0 -11
  114. package/dist/orchestrate-initiative.d.ts +0 -12
  115. package/dist/orchestrate-monitor.d.ts +0 -11
  116. package/dist/release.d.ts +0 -117
  117. package/dist/rotate-progress.d.ts +0 -48
  118. package/dist/session-coordinator.d.ts +0 -74
  119. package/dist/spawn-list.d.ts +0 -16
  120. package/dist/state-bootstrap.d.ts +0 -92
  121. package/dist/sync-templates.d.ts +0 -52
  122. package/dist/trace-gen.d.ts +0 -84
  123. package/dist/validate-agent-skills.d.ts +0 -50
  124. package/dist/validate-agent-sync.d.ts +0 -36
  125. package/dist/validate-backlog-sync.d.ts +0 -37
  126. package/dist/validate-skills-spec.d.ts +0 -40
  127. package/dist/validate.d.ts +0 -60
  128. package/dist/wu-block.d.ts +0 -16
  129. package/dist/wu-claim.d.ts +0 -74
  130. package/dist/wu-cleanup.d.ts +0 -35
  131. package/dist/wu-create.d.ts +0 -69
  132. package/dist/wu-delete.d.ts +0 -21
  133. package/dist/wu-deps.d.ts +0 -13
  134. package/dist/wu-done.d.ts +0 -225
  135. package/dist/wu-edit.d.ts +0 -63
  136. package/dist/wu-infer-lane.d.ts +0 -17
  137. package/dist/wu-preflight.d.ts +0 -47
  138. package/dist/wu-prune.d.ts +0 -16
  139. package/dist/wu-recover.d.ts +0 -37
  140. package/dist/wu-release.d.ts +0 -19
  141. package/dist/wu-repair.d.ts +0 -60
  142. package/dist/wu-spawn-completion.d.ts +0 -10
  143. package/dist/wu-spawn.d.ts +0 -192
  144. package/dist/wu-status.d.ts +0 -25
  145. package/dist/wu-unblock.d.ts +0 -16
  146. package/dist/wu-unlock-lane.d.ts +0 -19
  147. package/dist/wu-validate.d.ts +0 -16
@@ -0,0 +1,231 @@
1
+ #!/usr/bin/env node
2
+ /* eslint-disable no-console -- CLI tool requires console output */
3
+ /* eslint-disable security/detect-non-literal-fs-filename */
4
+ /**
5
+ * Plan Promote Command (WU-1313)
6
+ *
7
+ * Promotes a plan from draft to approved status.
8
+ * Validates that required sections are complete before approving.
9
+ *
10
+ * Usage:
11
+ * pnpm plan:promote --id WU-1313
12
+ * pnpm plan:promote --id INIT-001 --force # Skip validation
13
+ *
14
+ * Features:
15
+ * - Validates plan completeness (non-empty required sections)
16
+ * - Adds approved status and timestamp
17
+ * - Uses micro-worktree isolation for atomic commits
18
+ * - Idempotent: no error if already approved
19
+ *
20
+ * Context: WU-1313 (INIT-013 Plan Tooling)
21
+ */
22
+ import { getGitForCwd } from '@lumenflow/core/dist/git-adapter.js';
23
+ import { die } from '@lumenflow/core/dist/error-handler.js';
24
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
25
+ import { join } from 'node:path';
26
+ import { createWUParser, WU_OPTIONS } from '@lumenflow/core/dist/arg-parser.js';
27
+ import { ensureOnMain } from '@lumenflow/core/dist/wu-helpers.js';
28
+ import { withMicroWorktree } from '@lumenflow/core/dist/micro-worktree.js';
29
+ import { WU_PATHS } from '@lumenflow/core/dist/wu-paths.js';
30
+ import { todayISO } from '@lumenflow/core/dist/date-utils.js';
31
+ import { LOG_PREFIX as CORE_LOG_PREFIX } from '@lumenflow/core/dist/wu-constants.js';
32
+ /** Log prefix for console output */
33
+ export const LOG_PREFIX = CORE_LOG_PREFIX.PLAN_PROMOTE ?? '[plan:promote]';
34
+ /** Micro-worktree operation name */
35
+ const OPERATION_NAME = 'plan-promote';
36
+ /** WU ID pattern */
37
+ const WU_ID_PATTERN = /^WU-\d+$/;
38
+ /** Initiative ID pattern */
39
+ const INIT_ID_PATTERN = /^INIT-[A-Z0-9]+$/i;
40
+ /** Required sections that must have content */
41
+ const REQUIRED_SECTIONS = ['Goal', 'Scope', 'Approach'];
42
+ /** Status marker patterns */
43
+ const STATUS_APPROVED_PATTERN = /^Status:\s*approved/im;
44
+ /**
45
+ * Get the path to a plan file from its ID
46
+ *
47
+ * @param id - WU or Initiative ID
48
+ * @returns Path to plan file
49
+ * @throws Error if plan not found
50
+ */
51
+ export function getPlanPath(id) {
52
+ const plansDir = WU_PATHS.PLANS_DIR();
53
+ const planPath = join(plansDir, `${id}-plan.md`);
54
+ if (!existsSync(planPath)) {
55
+ die(`Plan not found for ${id}\n\n` +
56
+ `Expected path: ${planPath}\n\n` +
57
+ `Create it first with: pnpm plan:create --id ${id} --title "Title"`);
58
+ }
59
+ return planPath;
60
+ }
61
+ /**
62
+ * Validate that a plan has all required sections with content
63
+ *
64
+ * @param planPath - Path to plan file
65
+ * @returns Validation result with valid flag and errors array
66
+ */
67
+ export function validatePlanComplete(planPath) {
68
+ if (!existsSync(planPath)) {
69
+ return { valid: false, errors: ['Plan file not found'] };
70
+ }
71
+ const text = readFileSync(planPath, { encoding: 'utf-8' });
72
+ const errors = [];
73
+ for (const section of REQUIRED_SECTIONS) {
74
+ const sectionHeading = `## ${section}`;
75
+ const headingIndex = text.indexOf(sectionHeading);
76
+ if (headingIndex === -1) {
77
+ errors.push(`Missing required section: ${section}`);
78
+ continue;
79
+ }
80
+ // Find the content between this heading and the next
81
+ const afterHeading = text.substring(headingIndex + sectionHeading.length);
82
+ const nextHeadingIndex = afterHeading.indexOf('\n## ');
83
+ const sectionContent = nextHeadingIndex >= 0 ? afterHeading.substring(0, nextHeadingIndex) : afterHeading;
84
+ // Check if content is just whitespace, empty, or only comments
85
+ const trimmedContent = sectionContent
86
+ .split('\n')
87
+ .filter((line) => !line.trim().startsWith('<!--') && line.trim() !== '-->')
88
+ .join('\n')
89
+ .trim();
90
+ if (trimmedContent === '' || trimmedContent.length < 10) {
91
+ errors.push(`${section} section is empty or too short`);
92
+ }
93
+ }
94
+ return {
95
+ valid: errors.length === 0,
96
+ errors,
97
+ };
98
+ }
99
+ /**
100
+ * Promote a plan to approved status
101
+ *
102
+ * Adds Status: approved and Approved: date after the Created line.
103
+ *
104
+ * @param planPath - Path to plan file
105
+ * @returns True if changes were made, false if already approved
106
+ */
107
+ export function promotePlan(planPath) {
108
+ if (!existsSync(planPath)) {
109
+ die(`Plan file not found: ${planPath}`);
110
+ }
111
+ const text = readFileSync(planPath, { encoding: 'utf-8' });
112
+ // Check if already approved
113
+ if (STATUS_APPROVED_PATTERN.test(text)) {
114
+ console.log(`${LOG_PREFIX} Plan already approved (idempotent)`);
115
+ return false;
116
+ }
117
+ // Find the Created: line and insert status after it
118
+ // Use specific pattern to avoid backtracking (sonarjs/slow-regex)
119
+ const createdPattern = /^Created:\s*\S.*$/m;
120
+ const createdMatch = createdPattern.exec(text);
121
+ const today = todayISO();
122
+ const statusLines = `Status: approved\nApproved: ${today}`;
123
+ let newText;
124
+ if (createdMatch && createdMatch.index !== undefined) {
125
+ // Insert after Created: line
126
+ const insertPos = createdMatch.index + createdMatch[0].length;
127
+ newText = text.substring(0, insertPos) + '\n' + statusLines + text.substring(insertPos);
128
+ }
129
+ else {
130
+ // No Created: line found, insert after first heading
131
+ const firstHeadingPattern = /^# .+$/m;
132
+ const firstHeadingMatch = firstHeadingPattern.exec(text);
133
+ if (firstHeadingMatch && firstHeadingMatch.index !== undefined) {
134
+ const insertPos = firstHeadingMatch.index + firstHeadingMatch[0].length;
135
+ newText =
136
+ text.substring(0, insertPos) + '\n\n' + statusLines + '\n' + text.substring(insertPos);
137
+ }
138
+ else {
139
+ // Fallback: prepend to file
140
+ newText = statusLines + '\n\n' + text;
141
+ }
142
+ }
143
+ writeFileSync(planPath, newText, { encoding: 'utf-8' });
144
+ console.log(`${LOG_PREFIX} Plan promoted to approved status`);
145
+ return true;
146
+ }
147
+ /**
148
+ * Generate commit message for plan promote operation
149
+ *
150
+ * @param id - WU or Initiative ID
151
+ * @returns Commit message
152
+ */
153
+ export function getCommitMessage(id) {
154
+ const idLower = id.toLowerCase();
155
+ return `docs: promote ${idLower} plan to approved`;
156
+ }
157
+ async function main() {
158
+ const FORCE_OPTION = {
159
+ name: 'force',
160
+ flags: '-f, --force',
161
+ description: 'Skip validation and promote anyway',
162
+ };
163
+ const args = createWUParser({
164
+ name: 'plan-promote',
165
+ description: 'Promote a plan to approved status',
166
+ options: [WU_OPTIONS.id, FORCE_OPTION],
167
+ required: ['id'],
168
+ allowPositionalId: true,
169
+ });
170
+ const id = args.id;
171
+ const force = args.force;
172
+ // Validate ID format
173
+ if (!WU_ID_PATTERN.test(id) && !INIT_ID_PATTERN.test(id)) {
174
+ die(`Invalid ID format: "${id}"\n\n` + `Expected format: WU-XXX or INIT-XXX`);
175
+ }
176
+ console.log(`${LOG_PREFIX} Promoting plan for ${id}...`);
177
+ // Ensure on main for micro-worktree operations
178
+ await ensureOnMain(getGitForCwd());
179
+ try {
180
+ await withMicroWorktree({
181
+ operation: OPERATION_NAME,
182
+ id,
183
+ logPrefix: LOG_PREFIX,
184
+ pushOnly: true,
185
+ execute: async ({ worktreePath }) => {
186
+ const planRelPath = join(WU_PATHS.PLANS_DIR(), `${id}-plan.md`);
187
+ const planAbsPath = join(worktreePath, planRelPath);
188
+ if (!existsSync(planAbsPath)) {
189
+ die(`Plan not found for ${id}\n\n` +
190
+ `Expected path: ${planRelPath}\n\n` +
191
+ `Create it first with: pnpm plan:create --id ${id} --title "Title"`);
192
+ }
193
+ // Validate plan completeness (unless force)
194
+ if (!force) {
195
+ const validation = validatePlanComplete(planAbsPath);
196
+ if (!validation.valid) {
197
+ const errorList = validation.errors.map((e) => ` - ${e}`).join('\n');
198
+ die(`Plan validation failed:\n\n${errorList}\n\n` +
199
+ `Fix these issues or use --force to skip validation.`);
200
+ }
201
+ console.log(`${LOG_PREFIX} Plan validation passed`);
202
+ }
203
+ // Promote the plan
204
+ const changed = promotePlan(planAbsPath);
205
+ if (!changed) {
206
+ console.log(`${LOG_PREFIX} No changes needed (already approved)`);
207
+ }
208
+ return {
209
+ commitMessage: getCommitMessage(id),
210
+ files: [planRelPath],
211
+ };
212
+ },
213
+ });
214
+ console.log(`\n${LOG_PREFIX} Plan promoted successfully!`);
215
+ console.log(`\nPromotion Details:`);
216
+ console.log(` ID: ${id}`);
217
+ console.log(` Status: approved`);
218
+ }
219
+ catch (error) {
220
+ die(`Plan promotion failed: ${error.message}\n\n` +
221
+ `Micro-worktree cleanup was attempted automatically.\n` +
222
+ `If issue persists, check for orphaned branches: git branch | grep tmp/${OPERATION_NAME}`);
223
+ }
224
+ }
225
+ // Guard main() for testability
226
+ import { runCLI } from './cli-entry-point.js';
227
+ if (import.meta.main) {
228
+ void runCLI(main);
229
+ }
230
+ // Export for testing
231
+ export { main };
@@ -8,9 +8,16 @@
8
8
  * - Claude skills -> templates/vendors/claude/.claude/skills/
9
9
  * - Core docs (LUMENFLOW.md, constraints.md) -> templates/core/
10
10
  */
11
+ /* eslint-disable no-console -- CLI tool requires console output */
12
+ /* eslint-disable security/detect-non-literal-fs-filename -- CLI tool syncs templates from known paths */
13
+ /* eslint-disable security/detect-non-literal-regexp -- Dynamic date pattern for template substitution */
11
14
  import * as fs from 'node:fs';
12
15
  import * as path from 'node:path';
13
16
  import { createWUParser } from '@lumenflow/core';
17
+ // Directory name constants to avoid duplicate strings
18
+ const LUMENFLOW_DIR = '.lumenflow';
19
+ const CLAUDE_DIR = '.claude';
20
+ const SKILLS_DIR = 'skills';
14
21
  // Template variable patterns
15
22
  const DATE_PATTERN = /\d{4}-\d{2}-\d{2}/g;
16
23
  /**
@@ -29,6 +36,12 @@ const SYNC_TEMPLATES_OPTIONS = {
29
36
  description: 'Show detailed output',
30
37
  default: false,
31
38
  },
39
+ checkDrift: {
40
+ name: 'check-drift',
41
+ flags: '--check-drift',
42
+ description: 'Check for template drift without syncing (CI mode)',
43
+ default: false,
44
+ },
32
45
  };
33
46
  /**
34
47
  * Parse sync-templates command options
@@ -42,6 +55,7 @@ export function parseSyncTemplatesOptions() {
42
55
  return {
43
56
  dryRun: opts['dry-run'] ?? false,
44
57
  verbose: opts.verbose ?? false,
58
+ checkDrift: opts['check-drift'] ?? false,
45
59
  };
46
60
  }
47
61
  /**
@@ -119,8 +133,8 @@ export async function syncOnboardingDocs(projectRoot, dryRun = false) {
119
133
  */
120
134
  export async function syncSkillsToTemplates(projectRoot, dryRun = false) {
121
135
  const result = { synced: [], errors: [] };
122
- const sourceDir = path.join(projectRoot, '.claude', 'skills');
123
- const targetDir = path.join(getTemplatesDir(projectRoot), 'vendors', 'claude', '.claude', 'skills');
136
+ const sourceDir = path.join(projectRoot, CLAUDE_DIR, SKILLS_DIR);
137
+ const targetDir = path.join(getTemplatesDir(projectRoot), 'vendors', 'claude', CLAUDE_DIR, SKILLS_DIR);
124
138
  if (!fs.existsSync(sourceDir)) {
125
139
  result.errors.push(`Skills source directory not found: ${sourceDir}`);
126
140
  return result;
@@ -153,8 +167,8 @@ export async function syncCoreDocs(projectRoot, dryRun = false) {
153
167
  const lumenflowTarget = path.join(templatesDir, 'core', 'LUMENFLOW.md.template');
154
168
  syncFile(lumenflowSource, lumenflowTarget, projectRoot, result, dryRun);
155
169
  // Sync constraints.md
156
- const constraintsSource = path.join(projectRoot, '.lumenflow', 'constraints.md');
157
- const constraintsTarget = path.join(templatesDir, 'core', '.lumenflow', 'constraints.md.template');
170
+ const constraintsSource = path.join(projectRoot, LUMENFLOW_DIR, 'constraints.md');
171
+ const constraintsTarget = path.join(templatesDir, 'core', LUMENFLOW_DIR, 'constraints.md.template');
158
172
  syncFile(constraintsSource, constraintsTarget, projectRoot, result, dryRun);
159
173
  return result;
160
174
  }
@@ -167,12 +181,130 @@ export async function syncTemplates(projectRoot, dryRun = false) {
167
181
  const core = await syncCoreDocs(projectRoot, dryRun);
168
182
  return { onboarding, skills, core };
169
183
  }
184
+ /**
185
+ * Compare source file content with template content (ignoring date placeholders)
186
+ */
187
+ function compareContent(sourceContent, templateContent, projectRoot) {
188
+ // Convert source to template format for comparison
189
+ const convertedSource = convertToTemplate(sourceContent, projectRoot);
190
+ return convertedSource === templateContent;
191
+ }
192
+ /**
193
+ * Check if a single template file is in sync with its source
194
+ */
195
+ function checkFileDrift(sourcePath, templatePath, projectRoot) {
196
+ const relativePath = path.relative(projectRoot, templatePath);
197
+ if (!fs.existsSync(sourcePath)) {
198
+ return { isDrifting: false, relativePath }; // Source doesn't exist, can't drift
199
+ }
200
+ if (!fs.existsSync(templatePath)) {
201
+ return { isDrifting: true, relativePath }; // Template missing, definitely drifting
202
+ }
203
+ const sourceContent = fs.readFileSync(sourcePath, 'utf-8');
204
+ const templateContent = fs.readFileSync(templatePath, 'utf-8');
205
+ const isDrifting = !compareContent(sourceContent, templateContent, projectRoot);
206
+ return { isDrifting, relativePath };
207
+ }
208
+ /**
209
+ * Check for template drift - compares source docs with templates (WU-1353)
210
+ *
211
+ * This function compares source documents with their template counterparts
212
+ * to detect if templates have drifted out of sync. Used by CI to warn
213
+ * when templates need to be re-synced.
214
+ */
215
+ // eslint-disable-next-line sonarjs/cognitive-complexity -- Multi-category drift check requires nested iteration
216
+ export async function checkTemplateDrift(projectRoot) {
217
+ const driftingFiles = [];
218
+ const checkedFiles = [];
219
+ const templatesDir = getTemplatesDir(projectRoot);
220
+ // Check core docs
221
+ const coreChecks = [
222
+ {
223
+ source: path.join(projectRoot, 'LUMENFLOW.md'),
224
+ template: path.join(templatesDir, 'core', 'LUMENFLOW.md.template'),
225
+ },
226
+ {
227
+ source: path.join(projectRoot, LUMENFLOW_DIR, 'constraints.md'),
228
+ template: path.join(templatesDir, 'core', LUMENFLOW_DIR, 'constraints.md.template'),
229
+ },
230
+ ];
231
+ for (const check of coreChecks) {
232
+ const result = checkFileDrift(check.source, check.template, projectRoot);
233
+ checkedFiles.push(result.relativePath);
234
+ if (result.isDrifting) {
235
+ driftingFiles.push(result.relativePath);
236
+ }
237
+ }
238
+ // Check onboarding docs
239
+ const onboardingSourceDir = path.join(projectRoot, 'docs', '04-operations', '_frameworks', 'lumenflow', 'agent', 'onboarding');
240
+ const onboardingTargetDir = path.join(templatesDir, 'core', 'ai', 'onboarding');
241
+ if (fs.existsSync(onboardingSourceDir)) {
242
+ const files = fs.readdirSync(onboardingSourceDir).filter((f) => f.endsWith('.md'));
243
+ for (const file of files) {
244
+ const sourcePath = path.join(onboardingSourceDir, file);
245
+ const templatePath = path.join(onboardingTargetDir, `${file}.template`);
246
+ const result = checkFileDrift(sourcePath, templatePath, projectRoot);
247
+ checkedFiles.push(result.relativePath);
248
+ if (result.isDrifting) {
249
+ driftingFiles.push(result.relativePath);
250
+ }
251
+ }
252
+ }
253
+ // Check skills
254
+ const skillsSourceDir = path.join(projectRoot, CLAUDE_DIR, SKILLS_DIR);
255
+ const skillsTargetDir = path.join(templatesDir, 'vendors', 'claude', CLAUDE_DIR, SKILLS_DIR);
256
+ if (fs.existsSync(skillsSourceDir)) {
257
+ const skillDirs = fs
258
+ .readdirSync(skillsSourceDir, { withFileTypes: true })
259
+ .filter((d) => d.isDirectory())
260
+ .map((d) => d.name);
261
+ for (const skillName of skillDirs) {
262
+ const skillFile = path.join(skillsSourceDir, skillName, 'SKILL.md');
263
+ const templatePath = path.join(skillsTargetDir, skillName, 'SKILL.md.template');
264
+ if (fs.existsSync(skillFile)) {
265
+ const result = checkFileDrift(skillFile, templatePath, projectRoot);
266
+ checkedFiles.push(result.relativePath);
267
+ if (result.isDrifting) {
268
+ driftingFiles.push(result.relativePath);
269
+ }
270
+ }
271
+ }
272
+ }
273
+ return {
274
+ hasDrift: driftingFiles.length > 0,
275
+ driftingFiles,
276
+ checkedFiles,
277
+ };
278
+ }
170
279
  /**
171
280
  * CLI entry point
172
281
  */
282
+ // eslint-disable-next-line sonarjs/cognitive-complexity -- CLI main() handles multiple modes and output formatting
173
283
  export async function main() {
174
284
  const opts = parseSyncTemplatesOptions();
175
285
  const projectRoot = process.cwd();
286
+ // Check-drift mode: verify templates match source without syncing
287
+ if (opts.checkDrift) {
288
+ console.log('[sync-templates] Checking for template drift...');
289
+ const drift = await checkTemplateDrift(projectRoot);
290
+ if (opts.verbose) {
291
+ console.log(` Checked ${drift.checkedFiles.length} files`);
292
+ }
293
+ if (drift.hasDrift) {
294
+ console.log('\n[sync-templates] WARNING: Template drift detected!');
295
+ console.log(' The following templates are out of sync with their source:');
296
+ for (const file of drift.driftingFiles) {
297
+ console.log(` - ${file}`);
298
+ }
299
+ console.log('\n Run `pnpm sync:templates` to update templates.');
300
+ process.exitCode = 1;
301
+ }
302
+ else {
303
+ console.log('[sync-templates] All templates are in sync.');
304
+ }
305
+ return;
306
+ }
307
+ // Sync mode: update templates from source
176
308
  console.log('[sync-templates] Syncing internal docs to CLI templates...');
177
309
  if (opts.dryRun) {
178
310
  console.log(' (dry-run mode - no files will be written)');
@@ -208,5 +340,5 @@ export async function main() {
208
340
  // CLI entry point
209
341
  import { runCLI } from './cli-entry-point.js';
210
342
  if (import.meta.main) {
211
- runCLI(main);
343
+ void runCLI(main);
212
344
  }
package/dist/wu-block.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ /* eslint-disable no-console -- CLI tool requires console output */
2
3
  /**
3
4
  * WU Block Helper
4
5
  *
@@ -32,6 +33,8 @@ import { withMicroWorktree } from '@lumenflow/core/dist/micro-worktree.js';
32
33
  import { WUStateStore } from '@lumenflow/core/dist/wu-state-store.js';
33
34
  // WU-1603: Atomic lane locking - release lock when WU is blocked
34
35
  import { releaseLaneLock } from '@lumenflow/core/dist/lane-lock.js';
36
+ // WU-1325: Import lock policy getter to determine release behavior
37
+ import { getLockPolicyForLane } from '@lumenflow/core/dist/lane-checker.js';
35
38
  // ensureOnMain() moved to wu-helpers.ts (WU-1256)
36
39
  // ensureStaged() moved to git-staged-validator.ts (WU-1341)
37
40
  // defaultWorktreeFrom() moved to wu-paths.ts (WU-1341)
@@ -215,15 +218,23 @@ async function main() {
215
218
  await getGitForCwd().push(REMOTES.ORIGIN, BRANCHES.MAIN);
216
219
  }
217
220
  await handleWorktreeRemoval(args, doc);
218
- // WU-1603: Release lane lock when WU is blocked
219
- // This allows another WU to be claimed in the same lane
221
+ // WU-1325: Release lane lock when WU is blocked (only for lock_policy=active)
222
+ // For policy=all, lock is held through block/unblock cycle
223
+ // For policy=none, no lock exists to release
220
224
  try {
221
225
  const lane = doc.lane;
222
226
  if (lane) {
223
- const releaseResult = releaseLaneLock(lane, { wuId: id });
224
- if (releaseResult.released && !releaseResult.notFound) {
225
- console.log(`${LOG_PREFIX.BLOCK} Lane lock released for "${lane}"`);
227
+ const lockPolicy = getLockPolicyForLane(lane);
228
+ if (lockPolicy === 'active') {
229
+ const releaseResult = releaseLaneLock(lane, { wuId: id });
230
+ if (releaseResult.released && !releaseResult.notFound) {
231
+ console.log(`${LOG_PREFIX.BLOCK} Lane lock released for "${lane}" (lock_policy=active)`);
232
+ }
226
233
  }
234
+ else if (lockPolicy === 'all') {
235
+ console.log(`${LOG_PREFIX.BLOCK} Lane lock retained for "${lane}" (lock_policy=all)`);
236
+ }
237
+ // For policy=none, no lock exists - nothing to do
227
238
  }
228
239
  }
229
240
  catch (err) {
package/dist/wu-claim.js CHANGED
@@ -67,9 +67,13 @@ async function ensureCleanOrClaimOnlyWhenNoAuto() {
67
67
  .split(STRING_LITERALS.NEWLINE)
68
68
  .filter(Boolean)
69
69
  .filter((l) => l.startsWith('A ') || l.startsWith('M ') || l.startsWith('R '));
70
- const hasClaimFiles = staged.some((l) => l.includes('docs/04-operations/tasks/status.md') ||
71
- l.includes('docs/04-operations/tasks/backlog.md') ||
72
- /docs\/04-operations\/tasks\/wu\/WU-\d+\.yaml/.test(l));
70
+ // WU-1311: Use config-based paths instead of hardcoded docs/04-operations paths
71
+ const config = getConfig();
72
+ const wuDirPattern = config.directories.wuDir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
73
+ const wuYamlRegex = new RegExp(`${wuDirPattern}/WU-\\d+\\.yaml`);
74
+ const hasClaimFiles = staged.some((l) => l.includes(config.directories.statusPath) ||
75
+ l.includes(config.directories.backlogPath) ||
76
+ wuYamlRegex.test(l));
73
77
  if (!hasClaimFiles) {
74
78
  console.error(status);
75
79
  die('Stage claim-related files (status/backlog/WU YAML) before running with --no-auto.');
@@ -400,12 +404,12 @@ async function appendClaimEventOnly(stateDir, id, title, lane) {
400
404
  * @returns {string[]} List of files to commit
401
405
  */
402
406
  export function getWorktreeCommitFiles(wuId) {
407
+ // WU-1311: Use config-based paths instead of hardcoded docs/04-operations paths
408
+ const config = getConfig();
403
409
  return [
404
- `docs/04-operations/tasks/wu/${wuId}.yaml`,
410
+ `${config.directories.wuDir}/${wuId}.yaml`,
405
411
  LUMENFLOW_PATHS.WU_EVENTS, // WU-1740: Event store is source of truth
406
- // WU-1746: Explicitly NOT including:
407
- // - docs/04-operations/tasks/backlog.md
408
- // - docs/04-operations/tasks/status.md
412
+ // WU-1746: Explicitly NOT including backlog.md and status.md
409
413
  // These generated files cause merge conflicts when main advances
410
414
  ];
411
415
  }
@@ -694,7 +698,8 @@ function handleLaneOccupancy(laneCheck, lane, id, force) {
694
698
  ` 2. Choose a different lane\n` +
695
699
  ` 3. Increase wip_limit in .lumenflow.config.yaml\n` +
696
700
  ` 4. Use --force to override (P0 emergencies only)\n\n` +
697
- `To check lane status: grep "${STATUS_SECTIONS.IN_PROGRESS}" docs/04-operations/tasks/status.md`);
701
+ // WU-1311: Use config-based status path
702
+ `To check lane status: grep "${STATUS_SECTIONS.IN_PROGRESS}" ${getConfig().directories.statusPath}`);
698
703
  }
699
704
  /**
700
705
  * Handle code path overlap detection (WU-901)
@@ -726,13 +731,14 @@ function handleCodePathOverlap(WU_PATH, STATUS_PATH, id, args) {
726
731
  return ` - ${c.wuid}: ${displayedOverlaps}${suffix}`;
727
732
  })
728
733
  .join(STRING_LITERALS.NEWLINE);
734
+ // WU-1311: Use config-based status path in error message
729
735
  die(`Code path overlap detected with in-progress WUs:\n\n${conflictList}\n\n` +
730
736
  `Merge conflicts are guaranteed if both WUs proceed.\n\n` +
731
737
  `Options:\n` +
732
738
  ` 1. Wait for conflicting WU(s) to complete\n` +
733
739
  ` 2. Coordinate with agent working on conflicting WU\n` +
734
740
  ` 3. Use --force-overlap --reason "..." (emits telemetry for audit)\n\n` +
735
- `To check WU status: grep "${STATUS_SECTIONS.IN_PROGRESS}" docs/04-operations/tasks/status.md`);
741
+ `To check WU status: grep "${STATUS_SECTIONS.IN_PROGRESS}" ${getConfig().directories.statusPath}`);
736
742
  }
737
743
  if (args.forceOverlap) {
738
744
  if (!args.reason) {
package/dist/wu-create.js CHANGED
@@ -41,6 +41,7 @@ import { inferSubLane } from '@lumenflow/core/dist/lane-inference.js';
41
41
  import { parseBacklogFrontmatter } from '@lumenflow/core/dist/backlog-parser.js';
42
42
  import { createWUParser, WU_CREATE_OPTIONS, WU_OPTIONS } from '@lumenflow/core/dist/arg-parser.js';
43
43
  import { WU_PATHS } from '@lumenflow/core/dist/wu-paths.js';
44
+ import { getConfig } from '@lumenflow/core/dist/lumenflow-config.js';
44
45
  import { validateWU } from '@lumenflow/core/dist/wu-schema.js';
45
46
  import { getPlanPath, getPlanProtocolRef, getPlansDir, } from '@lumenflow/core/dist/lumenflow-home.js';
46
47
  import { validateSpecRefs } from '@lumenflow/core/dist/wu-create-validators.js';
@@ -57,6 +58,8 @@ import { validateSpecCompleteness } from '@lumenflow/core/dist/wu-done-validator
57
58
  import { readWU } from '@lumenflow/core/dist/wu-yaml.js';
58
59
  // WU-2253: Import WU spec linter for acceptance/code_paths validation
59
60
  import { lintWUSpec, formatLintErrors } from '@lumenflow/core/dist/wu-lint.js';
61
+ // WU-1329: Import path existence validators for strict mode
62
+ import { validateCodePathsExistence, validateTestPathsExistence, } from '@lumenflow/core/dist/wu-preflight-validators.js';
60
63
  // WU-1025: Import placeholder validator for inline content validation
61
64
  import { validateNoPlaceholders, buildPlaceholderErrorMessage, } from '@lumenflow/core/dist/wu-validator.js';
62
65
  // WU-1211: Import initiative validation for phase check
@@ -247,9 +250,24 @@ function buildWUContent({ id, lane, title, priority, type, created, opts, }) {
247
250
  ...(specRefs?.length && { spec_refs: specRefs }),
248
251
  };
249
252
  }
253
+ /**
254
+ * Validate WU spec for creation
255
+ *
256
+ * WU-1329: Strict mode (default) validates that code_paths and test_paths exist on disk.
257
+ * Use opts.strict = false to bypass path existence checks.
258
+ *
259
+ * @param params - Validation parameters
260
+ * @returns {{ valid: boolean, errors: string[] }}
261
+ */
250
262
  export function validateCreateSpec({ id, lane, title, priority, type, opts, }) {
251
263
  const errors = [];
252
264
  const effectiveType = type || DEFAULT_TYPE;
265
+ // WU-1329: Strict mode is the default
266
+ const strict = opts.strict !== false;
267
+ // WU-1329: Log when strict validation is bypassed
268
+ if (!strict) {
269
+ console.warn(`${LOG_PREFIX} WARNING: strict validation bypassed (--no-strict). Path existence checks skipped.`);
270
+ }
253
271
  if (!opts.description) {
254
272
  errors.push('--description is required');
255
273
  }
@@ -269,7 +287,9 @@ export function validateCreateSpec({ id, lane, title, priority, type, opts, }) {
269
287
  }
270
288
  }
271
289
  if (effectiveType === 'feature' && !opts.specRefs) {
272
- errors.push('--spec-refs is required for type: feature WUs');
290
+ errors.push('--spec-refs is required for type: feature WUs\n' +
291
+ ' Tip: Create a plan first with: pnpm plan:create --id <WU-ID> --title "..."\n' +
292
+ ' Then use --plan flag or --spec-refs lumenflow://plans/<WU-ID>-plan.md');
273
293
  }
274
294
  if (errors.length > 0) {
275
295
  return { valid: false, errors };
@@ -303,6 +323,29 @@ export function validateCreateSpec({ id, lane, title, priority, type, opts, }) {
303
323
  if (!completeness.valid) {
304
324
  return { valid: false, errors: completeness.errors };
305
325
  }
326
+ // WU-1329: Strict mode validates path existence
327
+ if (strict) {
328
+ const rootDir = process.cwd();
329
+ // Validate code_paths exist
330
+ if (opts.codePaths && opts.codePaths.length > 0) {
331
+ const codePathsResult = validateCodePathsExistence(opts.codePaths, rootDir);
332
+ if (!codePathsResult.valid) {
333
+ errors.push(...codePathsResult.errors);
334
+ }
335
+ }
336
+ // Validate test_paths exist (unit, e2e - not manual)
337
+ const testsObj = {
338
+ unit: opts.testPathsUnit || [],
339
+ e2e: opts.testPathsE2e || [],
340
+ };
341
+ const testPathsResult = validateTestPathsExistence(testsObj, rootDir);
342
+ if (!testPathsResult.valid) {
343
+ errors.push(...testPathsResult.errors);
344
+ }
345
+ if (errors.length > 0) {
346
+ return { valid: false, errors };
347
+ }
348
+ }
306
349
  return { valid: true, errors: [] };
307
350
  }
308
351
  /**
@@ -382,9 +425,10 @@ function updateBacklogInWorktree(worktreePath, id, lane, title) {
382
425
  const backlogRelativePath = WU_PATHS.BACKLOG();
383
426
  const backlogAbsolutePath = join(worktreePath, backlogRelativePath);
384
427
  if (!existsSync(backlogAbsolutePath)) {
428
+ // WU-1311: Use config-based backlog path in error message
385
429
  die(`Backlog not found in micro-worktree: ${backlogAbsolutePath}\n\n` +
386
430
  `Options:\n` +
387
- ` 1. Ensure backlog.md exists at docs/04-operations/tasks/backlog.md\n` +
431
+ ` 1. Ensure backlog.md exists at ${getConfig().directories.backlogPath}\n` +
388
432
  ` 2. Run from repository root directory`);
389
433
  }
390
434
  const { frontmatter, markdown } = parseBacklogFrontmatter(backlogAbsolutePath);
@@ -482,6 +526,8 @@ async function main() {
482
526
  WU_OPTIONS.uiPairingWus,
483
527
  // WU-1062: External plan options for wu:create
484
528
  WU_CREATE_OPTIONS.plan,
529
+ // WU-1329: Strict validation is default, --no-strict bypasses
530
+ WU_OPTIONS.noStrict,
485
531
  ],
486
532
  required: ['lane', 'title'], // WU-1246: --id is now optional (auto-generated if not provided)
487
533
  allowPositionalId: false,
@@ -558,6 +604,8 @@ async function main() {
558
604
  blocks: args.blocks,
559
605
  labels: args.labels,
560
606
  assignedTo,
607
+ // WU-1329: Strict validation is default, --no-strict bypasses
608
+ strict: !args.noStrict,
561
609
  },
562
610
  });
563
611
  if (!createSpecValidation.valid) {
package/dist/wu-deps.js CHANGED
@@ -15,6 +15,7 @@ import { die } from '@lumenflow/core/dist/error-handler.js';
15
15
  import { buildDependencyGraphAsync, renderASCII, renderMermaid, validateGraph, } from '@lumenflow/core/dist/dependency-graph.js';
16
16
  import { OUTPUT_FORMATS } from '@lumenflow/initiatives/dist/initiative-constants.js';
17
17
  import { PATTERNS } from '@lumenflow/core/dist/wu-constants.js';
18
+ import { getConfig } from '@lumenflow/core/dist/lumenflow-config.js';
18
19
  async function main() {
19
20
  const args = createWUParser({
20
21
  name: 'wu-deps',
@@ -33,7 +34,8 @@ async function main() {
33
34
  console.log(`[wu:deps] Building dependency graph...`);
34
35
  const graph = await buildDependencyGraphAsync();
35
36
  if (!graph.has(wuId)) {
36
- die(`WU not found in graph: ${wuId}\n\nEnsure the WU exists in docs/04-operations/tasks/wu/`);
37
+ // WU-1311: Use config-based WU directory path
38
+ die(`WU not found in graph: ${wuId}\n\nEnsure the WU exists in ${getConfig().directories.wuDir}/`);
37
39
  }
38
40
  const format = args.format || OUTPUT_FORMATS.ASCII;
39
41
  const depth = args.depth ? parseInt(args.depth, 10) : 3;