@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,239 @@
1
+ /**
2
+ * Tests for plan:link command (WU-1313)
3
+ *
4
+ * The plan:link command links existing plan files to WUs (via spec_refs)
5
+ * or initiatives (via related_plan). This replaces the initiative:plan command.
6
+ *
7
+ * TDD: These tests are written BEFORE the implementation.
8
+ */
9
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
10
+ import { existsSync, mkdirSync, rmSync, writeFileSync, readFileSync } from 'node:fs';
11
+ import { join } from 'node:path';
12
+ import { tmpdir } from 'node:os';
13
+ import { parseYAML, stringifyYAML } from '@lumenflow/core/dist/wu-yaml.js';
14
+ /** Test constants - avoid sonarjs/no-duplicate-string */
15
+ const TEST_WU_DIR = 'docs/04-operations/tasks/wu';
16
+ const TEST_INIT_DIR = 'docs/04-operations/tasks/initiatives';
17
+ const TEST_PLANS_DIR = 'docs/04-operations/plans';
18
+ const TEST_WU_ID = 'WU-1313';
19
+ const TEST_INIT_ID = 'INIT-001';
20
+ const TEST_WU_PLAN_URI = `lumenflow://plans/${TEST_WU_ID}-plan.md`;
21
+ const TEST_INIT_PLAN_URI = `lumenflow://plans/${TEST_INIT_ID}-plan.md`;
22
+ const TEST_LANE = 'Framework: CLI';
23
+ const TEST_INIT_SLUG = 'test-initiative';
24
+ const TEST_INIT_TITLE = 'Test Initiative';
25
+ const TEST_WU_TITLE = 'Test WU';
26
+ const TEST_STATUS_OPEN = 'open';
27
+ const TEST_DATE = '2026-01-25';
28
+ // Mock modules before importing
29
+ vi.mock('@lumenflow/core/dist/git-adapter.js', () => ({
30
+ getGitForCwd: vi.fn(() => ({
31
+ branch: vi.fn().mockResolvedValue({ current: 'main' }),
32
+ status: vi.fn().mockResolvedValue({ isClean: () => true }),
33
+ })),
34
+ }));
35
+ vi.mock('@lumenflow/core/dist/wu-helpers.js', () => ({
36
+ ensureOnMain: vi.fn().mockResolvedValue(undefined),
37
+ }));
38
+ vi.mock('@lumenflow/core/dist/micro-worktree.js', () => ({
39
+ withMicroWorktree: vi.fn(async ({ execute }) => {
40
+ const tempDir = join(tmpdir(), `plan-link-test-${Date.now()}`);
41
+ mkdirSync(tempDir, { recursive: true });
42
+ return execute({ worktreePath: tempDir });
43
+ }),
44
+ }));
45
+ describe('plan:link command', () => {
46
+ let tempDir;
47
+ let originalCwd;
48
+ beforeEach(() => {
49
+ tempDir = join(tmpdir(), `plan-link-test-${Date.now()}`);
50
+ mkdirSync(tempDir, { recursive: true });
51
+ originalCwd = process.cwd();
52
+ });
53
+ afterEach(() => {
54
+ process.chdir(originalCwd);
55
+ if (existsSync(tempDir)) {
56
+ rmSync(tempDir, { recursive: true, force: true });
57
+ }
58
+ vi.clearAllMocks();
59
+ });
60
+ describe('linkPlanToWU', () => {
61
+ it('should add spec_refs field to WU YAML', async () => {
62
+ const { linkPlanToWU } = await import('../plan-link.js');
63
+ // Setup mock WU file
64
+ const wuDir = join(tempDir, ...TEST_WU_DIR.split('/'));
65
+ mkdirSync(wuDir, { recursive: true });
66
+ const wuPath = join(wuDir, `${TEST_WU_ID}.yaml`);
67
+ const wuDoc = {
68
+ id: TEST_WU_ID,
69
+ title: TEST_WU_TITLE,
70
+ lane: TEST_LANE,
71
+ status: 'ready',
72
+ type: 'feature',
73
+ };
74
+ writeFileSync(wuPath, stringifyYAML(wuDoc));
75
+ // Link plan
76
+ const changed = linkPlanToWU(tempDir, TEST_WU_ID, TEST_WU_PLAN_URI);
77
+ expect(changed).toBe(true);
78
+ // Verify the file was updated
79
+ const updated = parseYAML(readFileSync(wuPath, 'utf-8'));
80
+ expect(updated.spec_refs).toContain(TEST_WU_PLAN_URI);
81
+ });
82
+ it('should append to existing spec_refs', async () => {
83
+ const { linkPlanToWU } = await import('../plan-link.js');
84
+ // Setup mock WU file with existing spec_refs
85
+ const wuDir = join(tempDir, ...TEST_WU_DIR.split('/'));
86
+ mkdirSync(wuDir, { recursive: true });
87
+ const wuPath = join(wuDir, `${TEST_WU_ID}.yaml`);
88
+ const wuDoc = {
89
+ id: TEST_WU_ID,
90
+ title: TEST_WU_TITLE,
91
+ lane: TEST_LANE,
92
+ status: 'ready',
93
+ type: 'feature',
94
+ spec_refs: ['lumenflow://plans/existing-plan.md'],
95
+ };
96
+ writeFileSync(wuPath, stringifyYAML(wuDoc));
97
+ // Link additional plan
98
+ const changed = linkPlanToWU(tempDir, TEST_WU_ID, TEST_WU_PLAN_URI);
99
+ expect(changed).toBe(true);
100
+ // Verify both refs are present
101
+ const updated = parseYAML(readFileSync(wuPath, 'utf-8'));
102
+ expect(updated.spec_refs).toContain('lumenflow://plans/existing-plan.md');
103
+ expect(updated.spec_refs).toContain(TEST_WU_PLAN_URI);
104
+ });
105
+ it('should be idempotent if plan already linked', async () => {
106
+ const { linkPlanToWU } = await import('../plan-link.js');
107
+ // Setup mock WU file with plan already linked
108
+ const wuDir = join(tempDir, ...TEST_WU_DIR.split('/'));
109
+ mkdirSync(wuDir, { recursive: true });
110
+ const wuPath = join(wuDir, `${TEST_WU_ID}.yaml`);
111
+ const wuDoc = {
112
+ id: TEST_WU_ID,
113
+ title: TEST_WU_TITLE,
114
+ lane: TEST_LANE,
115
+ status: 'ready',
116
+ type: 'feature',
117
+ spec_refs: [TEST_WU_PLAN_URI],
118
+ };
119
+ writeFileSync(wuPath, stringifyYAML(wuDoc));
120
+ // Link same plan again
121
+ const changed = linkPlanToWU(tempDir, TEST_WU_ID, TEST_WU_PLAN_URI);
122
+ expect(changed).toBe(false);
123
+ });
124
+ it('should throw if WU not found', async () => {
125
+ const { linkPlanToWU } = await import('../plan-link.js');
126
+ expect(() => linkPlanToWU(tempDir, 'WU-9999', 'lumenflow://plans/plan.md')).toThrow();
127
+ });
128
+ });
129
+ describe('linkPlanToInitiative', () => {
130
+ it('should add related_plan field to initiative YAML', async () => {
131
+ const { linkPlanToInitiative } = await import('../plan-link.js');
132
+ // Setup mock initiative file
133
+ const initDir = join(tempDir, ...TEST_INIT_DIR.split('/'));
134
+ mkdirSync(initDir, { recursive: true });
135
+ const initPath = join(initDir, `${TEST_INIT_ID}.yaml`);
136
+ const initDoc = {
137
+ id: TEST_INIT_ID,
138
+ slug: TEST_INIT_SLUG,
139
+ title: TEST_INIT_TITLE,
140
+ status: TEST_STATUS_OPEN,
141
+ created: TEST_DATE,
142
+ };
143
+ writeFileSync(initPath, stringifyYAML(initDoc));
144
+ // Link plan
145
+ const changed = linkPlanToInitiative(tempDir, TEST_INIT_ID, TEST_INIT_PLAN_URI);
146
+ expect(changed).toBe(true);
147
+ // Verify the file was updated
148
+ const updated = parseYAML(readFileSync(initPath, 'utf-8'));
149
+ expect(updated.related_plan).toBe(TEST_INIT_PLAN_URI);
150
+ });
151
+ it('should be idempotent if plan already linked', async () => {
152
+ const { linkPlanToInitiative } = await import('../plan-link.js');
153
+ // Setup mock initiative file with plan already linked
154
+ const initDir = join(tempDir, ...TEST_INIT_DIR.split('/'));
155
+ mkdirSync(initDir, { recursive: true });
156
+ const initPath = join(initDir, `${TEST_INIT_ID}.yaml`);
157
+ const initDoc = {
158
+ id: TEST_INIT_ID,
159
+ slug: TEST_INIT_SLUG,
160
+ title: TEST_INIT_TITLE,
161
+ status: TEST_STATUS_OPEN,
162
+ created: TEST_DATE,
163
+ related_plan: TEST_INIT_PLAN_URI,
164
+ };
165
+ writeFileSync(initPath, stringifyYAML(initDoc));
166
+ // Link same plan again
167
+ const changed = linkPlanToInitiative(tempDir, TEST_INIT_ID, TEST_INIT_PLAN_URI);
168
+ expect(changed).toBe(false);
169
+ });
170
+ it('should warn but proceed if replacing existing plan', async () => {
171
+ const { linkPlanToInitiative } = await import('../plan-link.js');
172
+ const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
173
+ // Setup mock initiative with different plan
174
+ const initDir = join(tempDir, ...TEST_INIT_DIR.split('/'));
175
+ mkdirSync(initDir, { recursive: true });
176
+ const initPath = join(initDir, `${TEST_INIT_ID}.yaml`);
177
+ const initDoc = {
178
+ id: TEST_INIT_ID,
179
+ slug: TEST_INIT_SLUG,
180
+ title: TEST_INIT_TITLE,
181
+ status: TEST_STATUS_OPEN,
182
+ created: TEST_DATE,
183
+ related_plan: 'lumenflow://plans/old-plan.md',
184
+ };
185
+ writeFileSync(initPath, stringifyYAML(initDoc));
186
+ // Link new plan
187
+ const changed = linkPlanToInitiative(tempDir, TEST_INIT_ID, 'lumenflow://plans/new-plan.md');
188
+ expect(changed).toBe(true);
189
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Replacing existing'));
190
+ consoleSpy.mockRestore();
191
+ });
192
+ });
193
+ describe('validatePlanExists', () => {
194
+ it('should pass for existing plan file', async () => {
195
+ const { validatePlanExists } = await import('../plan-link.js');
196
+ const plansDir = join(tempDir, ...TEST_PLANS_DIR.split('/'));
197
+ mkdirSync(plansDir, { recursive: true });
198
+ const planPath = join(plansDir, `${TEST_WU_ID}-plan.md`);
199
+ writeFileSync(planPath, '# Plan');
200
+ expect(() => validatePlanExists(tempDir, TEST_WU_PLAN_URI)).not.toThrow();
201
+ });
202
+ it('should throw for non-existent plan file', async () => {
203
+ const { validatePlanExists } = await import('../plan-link.js');
204
+ expect(() => validatePlanExists(tempDir, 'lumenflow://plans/nonexistent.md')).toThrow();
205
+ });
206
+ });
207
+ describe('resolveTargetType', () => {
208
+ it('should detect WU IDs', async () => {
209
+ const { resolveTargetType } = await import('../plan-link.js');
210
+ expect(resolveTargetType(TEST_WU_ID)).toBe('wu');
211
+ expect(resolveTargetType('WU-001')).toBe('wu');
212
+ expect(resolveTargetType('WU-99999')).toBe('wu');
213
+ });
214
+ it('should detect initiative IDs', async () => {
215
+ const { resolveTargetType } = await import('../plan-link.js');
216
+ expect(resolveTargetType(TEST_INIT_ID)).toBe('initiative');
217
+ expect(resolveTargetType('INIT-TOOLING')).toBe('initiative');
218
+ });
219
+ it('should throw for invalid IDs', async () => {
220
+ const { resolveTargetType } = await import('../plan-link.js');
221
+ expect(() => resolveTargetType('invalid')).toThrow();
222
+ expect(() => resolveTargetType('')).toThrow();
223
+ });
224
+ });
225
+ });
226
+ describe('plan:link CLI exports', () => {
227
+ it('should export main function for CLI entry', async () => {
228
+ const planLink = await import('../plan-link.js');
229
+ expect(typeof planLink.main).toBe('function');
230
+ });
231
+ it('should export all required functions', async () => {
232
+ const planLink = await import('../plan-link.js');
233
+ expect(typeof planLink.linkPlanToWU).toBe('function');
234
+ expect(typeof planLink.linkPlanToInitiative).toBe('function');
235
+ expect(typeof planLink.validatePlanExists).toBe('function');
236
+ expect(typeof planLink.resolveTargetType).toBe('function');
237
+ expect(typeof planLink.LOG_PREFIX).toBe('string');
238
+ });
239
+ });
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Tests for plan:promote command (WU-1313)
3
+ *
4
+ * The plan:promote command promotes a plan from draft to approved status,
5
+ * or creates WUs from plan sections.
6
+ *
7
+ * TDD: These tests are written BEFORE the implementation.
8
+ */
9
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
10
+ import { existsSync, mkdirSync, rmSync, writeFileSync, readFileSync } from 'node:fs';
11
+ import { join } from 'node:path';
12
+ import { tmpdir } from 'node:os';
13
+ /** Test constants - avoid sonarjs/no-duplicate-string */
14
+ const TEST_PLANS_DIR = 'docs/04-operations/plans';
15
+ const TEST_WU_ID = 'WU-1313';
16
+ // Mock modules before importing
17
+ vi.mock('@lumenflow/core/dist/git-adapter.js', () => ({
18
+ getGitForCwd: vi.fn(() => ({
19
+ branch: vi.fn().mockResolvedValue({ current: 'main' }),
20
+ status: vi.fn().mockResolvedValue({ isClean: () => true }),
21
+ })),
22
+ }));
23
+ vi.mock('@lumenflow/core/dist/wu-helpers.js', () => ({
24
+ ensureOnMain: vi.fn().mockResolvedValue(undefined),
25
+ }));
26
+ vi.mock('@lumenflow/core/dist/micro-worktree.js', () => ({
27
+ withMicroWorktree: vi.fn(async ({ execute }) => {
28
+ const tempDir = join(tmpdir(), `plan-promote-test-${Date.now()}`);
29
+ mkdirSync(tempDir, { recursive: true });
30
+ return execute({ worktreePath: tempDir });
31
+ }),
32
+ }));
33
+ describe('plan:promote command', () => {
34
+ let tempDir;
35
+ let originalCwd;
36
+ beforeEach(() => {
37
+ tempDir = join(tmpdir(), `plan-promote-test-${Date.now()}`);
38
+ mkdirSync(tempDir, { recursive: true });
39
+ originalCwd = process.cwd();
40
+ });
41
+ afterEach(() => {
42
+ process.chdir(originalCwd);
43
+ if (existsSync(tempDir)) {
44
+ rmSync(tempDir, { recursive: true, force: true });
45
+ }
46
+ vi.clearAllMocks();
47
+ });
48
+ describe('promotePlan', () => {
49
+ it('should add approved status marker to plan', async () => {
50
+ const { promotePlan } = await import('../plan-promote.js');
51
+ // Setup plan file
52
+ const plansDir = join(tempDir, ...TEST_PLANS_DIR.split('/'));
53
+ mkdirSync(plansDir, { recursive: true });
54
+ const planPath = join(plansDir, `${TEST_WU_ID}-plan.md`);
55
+ writeFileSync(planPath, `# WU-1313 Plan
56
+
57
+ Created: 2026-02-01
58
+
59
+ ## Goal
60
+
61
+ Implement plan tooling.
62
+ `);
63
+ // Promote plan
64
+ const changed = promotePlan(planPath);
65
+ expect(changed).toBe(true);
66
+ const content = readFileSync(planPath, 'utf-8');
67
+ expect(content).toContain('Status: approved');
68
+ expect(content).toContain('Approved:');
69
+ });
70
+ it('should return false if plan already approved', async () => {
71
+ const { promotePlan } = await import('../plan-promote.js');
72
+ // Setup plan file with approved status
73
+ const plansDir = join(tempDir, ...TEST_PLANS_DIR.split('/'));
74
+ mkdirSync(plansDir, { recursive: true });
75
+ const planPath = join(plansDir, `${TEST_WU_ID}-plan.md`);
76
+ writeFileSync(planPath, `# WU-1313 Plan
77
+
78
+ Created: 2026-02-01
79
+ Status: approved
80
+ Approved: 2026-02-01
81
+
82
+ ## Goal
83
+
84
+ Implement plan tooling.
85
+ `);
86
+ // Try to promote again
87
+ const changed = promotePlan(planPath);
88
+ expect(changed).toBe(false);
89
+ });
90
+ it('should throw if plan file not found', async () => {
91
+ const { promotePlan } = await import('../plan-promote.js');
92
+ const planPath = join(tempDir, 'nonexistent.md');
93
+ expect(() => promotePlan(planPath)).toThrow();
94
+ });
95
+ });
96
+ describe('validatePlanComplete', () => {
97
+ it('should pass for complete plan', async () => {
98
+ const { validatePlanComplete } = await import('../plan-promote.js');
99
+ // Setup complete plan file
100
+ const plansDir = join(tempDir, ...TEST_PLANS_DIR.split('/'));
101
+ mkdirSync(plansDir, { recursive: true });
102
+ const planPath = join(plansDir, `${TEST_WU_ID}-plan.md`);
103
+ writeFileSync(planPath, `# WU-1313 Plan
104
+
105
+ Created: 2026-02-01
106
+
107
+ ## Goal
108
+
109
+ Clear goal statement here.
110
+
111
+ ## Scope
112
+
113
+ - In scope: A
114
+ - Out of scope: B
115
+
116
+ ## Approach
117
+
118
+ Step 1: Do X
119
+ Step 2: Do Y
120
+ `);
121
+ const result = validatePlanComplete(planPath);
122
+ expect(result.valid).toBe(true);
123
+ expect(result.errors).toHaveLength(0);
124
+ });
125
+ it('should fail for plan with empty sections', async () => {
126
+ const { validatePlanComplete } = await import('../plan-promote.js');
127
+ // Setup incomplete plan file
128
+ const plansDir = join(tempDir, ...TEST_PLANS_DIR.split('/'));
129
+ mkdirSync(plansDir, { recursive: true });
130
+ const planPath = join(plansDir, `${TEST_WU_ID}-plan.md`);
131
+ writeFileSync(planPath, `# WU-1313 Plan
132
+
133
+ Created: 2026-02-01
134
+
135
+ ## Goal
136
+
137
+ ## Scope
138
+
139
+ ## Approach
140
+ `);
141
+ const result = validatePlanComplete(planPath);
142
+ expect(result.valid).toBe(false);
143
+ expect(result.errors.length).toBeGreaterThan(0);
144
+ expect(result.errors.some((e) => e.includes('Goal'))).toBe(true);
145
+ });
146
+ });
147
+ describe('getPlanPath', () => {
148
+ it('should resolve plan path from ID', async () => {
149
+ const { getPlanPath } = await import('../plan-promote.js');
150
+ // Setup plan file
151
+ const plansDir = join(tempDir, ...TEST_PLANS_DIR.split('/'));
152
+ mkdirSync(plansDir, { recursive: true });
153
+ const planPath = join(plansDir, `${TEST_WU_ID}-plan.md`);
154
+ writeFileSync(planPath, '# Plan');
155
+ process.chdir(tempDir);
156
+ const resolved = getPlanPath('WU-1313');
157
+ expect(resolved).toContain(`${TEST_WU_ID}-plan.md`);
158
+ });
159
+ });
160
+ describe('getCommitMessage', () => {
161
+ it('should generate correct commit message', async () => {
162
+ const { getCommitMessage } = await import('../plan-promote.js');
163
+ expect(getCommitMessage('WU-1313')).toBe('docs: promote wu-1313 plan to approved');
164
+ expect(getCommitMessage('INIT-001')).toBe('docs: promote init-001 plan to approved');
165
+ });
166
+ });
167
+ });
168
+ describe('plan:promote CLI exports', () => {
169
+ it('should export main function for CLI entry', async () => {
170
+ const planPromote = await import('../plan-promote.js');
171
+ expect(typeof planPromote.main).toBe('function');
172
+ });
173
+ it('should export all required functions', async () => {
174
+ const planPromote = await import('../plan-promote.js');
175
+ expect(typeof planPromote.promotePlan).toBe('function');
176
+ expect(typeof planPromote.validatePlanComplete).toBe('function');
177
+ expect(typeof planPromote.getPlanPath).toBe('function');
178
+ expect(typeof planPromote.getCommitMessage).toBe('function');
179
+ expect(typeof planPromote.LOG_PREFIX).toBe('string');
180
+ });
181
+ });
@@ -0,0 +1,219 @@
1
+ /**
2
+ * @file templates-sync.test.ts
3
+ * Tests for templates synchronization and drift detection (WU-1353)
4
+ */
5
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
6
+ import * as fs from 'node:fs';
7
+ import * as path from 'node:path';
8
+ import * as os from 'node:os';
9
+ import { syncTemplates, syncOnboardingDocs, syncCoreDocs, convertToTemplate, checkTemplateDrift, } from '../sync-templates.js';
10
+ // Constants for frequently used path segments (sonarjs/no-duplicate-string)
11
+ const PACKAGES_DIR = 'packages';
12
+ const LUMENFLOW_SCOPE = '@lumenflow';
13
+ const CLI_DIR = 'cli';
14
+ const TEMPLATES_DIR = 'templates';
15
+ const CORE_DIR = 'core';
16
+ const LUMENFLOW_DOT_DIR = '.lumenflow';
17
+ const CONSTRAINTS_FILE = 'constraints.md';
18
+ const CONSTRAINTS_TEMPLATE = 'constraints.md.template';
19
+ describe('templates-sync', () => {
20
+ let tempDir;
21
+ beforeEach(() => {
22
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'templates-sync-test-'));
23
+ });
24
+ afterEach(() => {
25
+ if (tempDir && fs.existsSync(tempDir)) {
26
+ fs.rmSync(tempDir, { recursive: true, force: true });
27
+ }
28
+ });
29
+ describe('convertToTemplate', () => {
30
+ it('should replace dates with {{DATE}} placeholder', () => {
31
+ const content = 'Updated: 2026-02-02\nCreated: 2025-01-15';
32
+ const result = convertToTemplate(content, '/home/test/project');
33
+ expect(result).toBe('Updated: {{DATE}}\nCreated: {{DATE}}');
34
+ });
35
+ it('should preserve content without dates', () => {
36
+ const content = '# Title\n\nSome content without dates.';
37
+ const result = convertToTemplate(content, '/home/test/project');
38
+ expect(result).toBe(content);
39
+ });
40
+ });
41
+ describe('syncCoreDocs', () => {
42
+ beforeEach(() => {
43
+ // Set up directory structure
44
+ const templatesDir = path.join(tempDir, PACKAGES_DIR, LUMENFLOW_SCOPE, CLI_DIR, TEMPLATES_DIR, CORE_DIR, LUMENFLOW_DOT_DIR);
45
+ fs.mkdirSync(templatesDir, { recursive: true });
46
+ // Create source constraints.md with v1.1 content
47
+ const lumenflowDir = path.join(tempDir, LUMENFLOW_DOT_DIR);
48
+ fs.mkdirSync(lumenflowDir, { recursive: true });
49
+ fs.writeFileSync(path.join(lumenflowDir, CONSTRAINTS_FILE), `# LumenFlow Constraints Capsule
50
+
51
+ **Version:** 1.1
52
+ **Last updated:** 2026-02-02
53
+
54
+ This document contains the 7 non-negotiable constraints.
55
+
56
+ ### 1. Worktree Discipline and Git Safety
57
+
58
+ **MANDATORY PRE-WRITE CHECK**
59
+
60
+ **NEVER "QUICK FIX" ON MAIN**
61
+ `);
62
+ // Create LUMENFLOW.md
63
+ fs.writeFileSync(path.join(tempDir, 'LUMENFLOW.md'), `# LumenFlow Workflow Guide
64
+
65
+ **Last updated:** 2026-02-02
66
+
67
+ ## Critical Rule: Use wu:prep Then wu:done
68
+ `);
69
+ });
70
+ it('should sync constraints.md to template', async () => {
71
+ const result = await syncCoreDocs(tempDir, false);
72
+ expect(result.errors).toHaveLength(0);
73
+ expect(result.synced).toContain(`${PACKAGES_DIR}/${LUMENFLOW_SCOPE}/${CLI_DIR}/${TEMPLATES_DIR}/${CORE_DIR}/${LUMENFLOW_DOT_DIR}/${CONSTRAINTS_TEMPLATE}`);
74
+ // Verify template content
75
+ const templatePath = path.join(tempDir, PACKAGES_DIR, LUMENFLOW_SCOPE, CLI_DIR, TEMPLATES_DIR, CORE_DIR, LUMENFLOW_DOT_DIR, CONSTRAINTS_TEMPLATE);
76
+ const templateContent = fs.readFileSync(templatePath, 'utf-8');
77
+ // Should have {{DATE}} placeholder
78
+ expect(templateContent).toContain('{{DATE}}');
79
+ expect(templateContent).not.toContain('2026-02-02');
80
+ // Should have v1.1 content markers
81
+ expect(templateContent).toContain('Version:** 1.1');
82
+ expect(templateContent).toContain('7 non-negotiable constraints');
83
+ expect(templateContent).toContain('MANDATORY PRE-WRITE CHECK');
84
+ expect(templateContent).toContain('NEVER "QUICK FIX" ON MAIN');
85
+ });
86
+ it('should use dry-run mode without writing files', async () => {
87
+ // First, ensure no template exists
88
+ const templatePath = path.join(tempDir, PACKAGES_DIR, LUMENFLOW_SCOPE, CLI_DIR, TEMPLATES_DIR, CORE_DIR, LUMENFLOW_DOT_DIR, CONSTRAINTS_TEMPLATE);
89
+ // Remove if it exists from beforeEach
90
+ if (fs.existsSync(templatePath)) {
91
+ fs.unlinkSync(templatePath);
92
+ }
93
+ const result = await syncCoreDocs(tempDir, true);
94
+ expect(result.errors).toHaveLength(0);
95
+ expect(result.synced.length).toBeGreaterThan(0);
96
+ expect(fs.existsSync(templatePath)).toBe(false);
97
+ });
98
+ });
99
+ describe('syncOnboardingDocs', () => {
100
+ const ONBOARDING_SUBPATH = [
101
+ 'docs',
102
+ '04-operations',
103
+ '_frameworks',
104
+ 'lumenflow',
105
+ 'agent',
106
+ 'onboarding',
107
+ ];
108
+ const FIRST_WU_MISTAKES_FILE = 'first-wu-mistakes.md';
109
+ beforeEach(() => {
110
+ // Set up onboarding source directory
111
+ const onboardingDir = path.join(tempDir, ...ONBOARDING_SUBPATH);
112
+ fs.mkdirSync(onboardingDir, { recursive: true });
113
+ // Create first-wu-mistakes.md with v1.1 content (11 mistakes)
114
+ fs.writeFileSync(path.join(onboardingDir, FIRST_WU_MISTAKES_FILE), `# First WU Mistakes
115
+
116
+ **Last updated:** 2026-02-02
117
+
118
+ ## Mistake 1: Not Using Worktrees
119
+
120
+ pnpm wu:prep --id WU-123
121
+
122
+ ## Mistake 11: "Quick Fixing" on Main
123
+
124
+ ## Quick Checklist
125
+
126
+ - [ ] Check spec_refs for plans
127
+ `);
128
+ // Set up target directory
129
+ const templatesDir = path.join(tempDir, PACKAGES_DIR, LUMENFLOW_SCOPE, CLI_DIR, TEMPLATES_DIR, CORE_DIR, 'ai', 'onboarding');
130
+ fs.mkdirSync(templatesDir, { recursive: true });
131
+ });
132
+ it('should sync first-wu-mistakes.md to template', async () => {
133
+ const result = await syncOnboardingDocs(tempDir, false);
134
+ expect(result.errors).toHaveLength(0);
135
+ expect(result.synced).toContain(`${PACKAGES_DIR}/${LUMENFLOW_SCOPE}/${CLI_DIR}/${TEMPLATES_DIR}/${CORE_DIR}/ai/onboarding/${FIRST_WU_MISTAKES_FILE}.template`);
136
+ // Verify template content
137
+ const templatePath = path.join(tempDir, PACKAGES_DIR, LUMENFLOW_SCOPE, CLI_DIR, TEMPLATES_DIR, CORE_DIR, 'ai', 'onboarding', `${FIRST_WU_MISTAKES_FILE}.template`);
138
+ const templateContent = fs.readFileSync(templatePath, 'utf-8');
139
+ // Should have {{DATE}} placeholder
140
+ expect(templateContent).toContain('{{DATE}}');
141
+ expect(templateContent).not.toContain('2026-02-02');
142
+ // Should have v1.1 content markers
143
+ expect(templateContent).toContain('Mistake 11:');
144
+ expect(templateContent).toContain('Quick Fixing" on Main');
145
+ expect(templateContent).toContain('wu:prep');
146
+ expect(templateContent).toContain('spec_refs');
147
+ });
148
+ });
149
+ describe('checkTemplateDrift', () => {
150
+ beforeEach(() => {
151
+ // Set up source files
152
+ const lumenflowDir = path.join(tempDir, LUMENFLOW_DOT_DIR);
153
+ fs.mkdirSync(lumenflowDir, { recursive: true });
154
+ fs.writeFileSync(path.join(lumenflowDir, CONSTRAINTS_FILE), `# Constraints
155
+ **Version:** 1.1
156
+ **Last updated:** 2026-02-02
157
+ 7 constraints`);
158
+ // Set up template directory
159
+ const templatesDir = path.join(tempDir, PACKAGES_DIR, LUMENFLOW_SCOPE, CLI_DIR, TEMPLATES_DIR, CORE_DIR, LUMENFLOW_DOT_DIR);
160
+ fs.mkdirSync(templatesDir, { recursive: true });
161
+ });
162
+ it('should detect drift when template is outdated', async () => {
163
+ // Create outdated template (v1.0, 6 constraints)
164
+ const templatePath = path.join(tempDir, PACKAGES_DIR, LUMENFLOW_SCOPE, CLI_DIR, TEMPLATES_DIR, CORE_DIR, LUMENFLOW_DOT_DIR, CONSTRAINTS_TEMPLATE);
165
+ fs.writeFileSync(templatePath, `# Constraints
166
+ **Version:** 1.0
167
+ **Last updated:** {{DATE}}
168
+ 6 constraints`);
169
+ const drift = await checkTemplateDrift(tempDir);
170
+ expect(drift.hasDrift).toBe(true);
171
+ expect(drift.driftingFiles.length).toBeGreaterThan(0);
172
+ expect(drift.driftingFiles.some((f) => f.includes(CONSTRAINTS_FILE))).toBe(true);
173
+ });
174
+ it('should report no drift when templates are in sync', async () => {
175
+ // First sync templates
176
+ await syncCoreDocs(tempDir, false);
177
+ // Then check for drift
178
+ const drift = await checkTemplateDrift(tempDir);
179
+ // After sync, constraints should not be drifting
180
+ expect(drift.driftingFiles.filter((f) => f.includes(CONSTRAINTS_FILE))).toHaveLength(0);
181
+ });
182
+ it('should return detailed drift report', async () => {
183
+ // Create outdated template
184
+ const templatePath = path.join(tempDir, PACKAGES_DIR, LUMENFLOW_SCOPE, CLI_DIR, TEMPLATES_DIR, CORE_DIR, LUMENFLOW_DOT_DIR, CONSTRAINTS_TEMPLATE);
185
+ fs.writeFileSync(templatePath, 'outdated content');
186
+ const drift = await checkTemplateDrift(tempDir);
187
+ expect(drift.hasDrift).toBe(true);
188
+ expect(drift.driftingFiles).toBeDefined();
189
+ expect(Array.isArray(drift.driftingFiles)).toBe(true);
190
+ });
191
+ });
192
+ describe('syncTemplates (full sync)', () => {
193
+ beforeEach(() => {
194
+ // Set up minimal directory structure
195
+ const lumenflowDir = path.join(tempDir, LUMENFLOW_DOT_DIR);
196
+ fs.mkdirSync(lumenflowDir, { recursive: true });
197
+ fs.writeFileSync(path.join(lumenflowDir, CONSTRAINTS_FILE), 'content');
198
+ fs.writeFileSync(path.join(tempDir, 'LUMENFLOW.md'), 'content');
199
+ const onboardingDir = path.join(tempDir, 'docs', '04-operations', '_frameworks', 'lumenflow', 'agent', 'onboarding');
200
+ fs.mkdirSync(onboardingDir, { recursive: true });
201
+ fs.writeFileSync(path.join(onboardingDir, 'first-wu-mistakes.md'), 'content');
202
+ const skillsDir = path.join(tempDir, '.claude', 'skills', 'test-skill');
203
+ fs.mkdirSync(skillsDir, { recursive: true });
204
+ fs.writeFileSync(path.join(skillsDir, 'SKILL.md'), 'skill content');
205
+ });
206
+ it('should sync all template categories', async () => {
207
+ const result = await syncTemplates(tempDir, false);
208
+ expect(result.core.errors).toHaveLength(0);
209
+ expect(result.onboarding.errors).toHaveLength(0);
210
+ expect(result.skills.errors).toHaveLength(0);
211
+ // Should sync at least constraints and LUMENFLOW
212
+ expect(result.core.synced.length).toBeGreaterThanOrEqual(2);
213
+ // Should sync onboarding docs
214
+ expect(result.onboarding.synced.length).toBeGreaterThanOrEqual(1);
215
+ // Should sync skills
216
+ expect(result.skills.synced.length).toBeGreaterThanOrEqual(1);
217
+ });
218
+ });
219
+ });