@lumenflow/cli 2.10.0 → 2.12.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 (255) hide show
  1. package/README.md +28 -18
  2. package/dist/__tests__/commands.test.js +198 -2
  3. package/dist/__tests__/gates-integration-tests.test.js +112 -0
  4. package/dist/__tests__/init-docs-structure.test.js +33 -0
  5. package/dist/__tests__/init.test.js +225 -0
  6. package/dist/__tests__/initiative-add-wu.test.js +71 -1
  7. package/dist/__tests__/no-beacon-references-docs.test.js +30 -0
  8. package/dist/__tests__/no-beacon-references.test.js +39 -0
  9. package/dist/__tests__/safe-git.test.js +4 -4
  10. package/dist/__tests__/wu-create-required-fields.test.js +22 -0
  11. package/dist/__tests__/wu-create.test.js +121 -0
  12. package/dist/__tests__/wu-done-docs-only-policy.test.js +20 -0
  13. package/dist/__tests__/wu-prep-default-exec.test.js +35 -0
  14. package/dist/__tests__/wu-prep.test.js +32 -0
  15. package/dist/__tests__/wu-validate.test.js +36 -0
  16. package/dist/agent-issues-query.js +1 -0
  17. package/dist/agent-issues-query.js.map +1 -0
  18. package/dist/agent-log-issue.js +1 -0
  19. package/dist/agent-log-issue.js.map +1 -0
  20. package/dist/agent-session-end.js +1 -0
  21. package/dist/agent-session-end.js.map +1 -0
  22. package/dist/agent-session.js +1 -0
  23. package/dist/agent-session.js.map +1 -0
  24. package/dist/backlog-prune.js +2 -0
  25. package/dist/backlog-prune.js.map +1 -0
  26. package/dist/cli-entry-point.js +1 -0
  27. package/dist/cli-entry-point.js.map +1 -0
  28. package/dist/commands/integrate.js +1 -0
  29. package/dist/commands/integrate.js.map +1 -0
  30. package/dist/commands.js +56 -77
  31. package/dist/commands.js.map +1 -0
  32. package/dist/deps-add.js +1 -0
  33. package/dist/deps-add.js.map +1 -0
  34. package/dist/deps-remove.js +1 -0
  35. package/dist/deps-remove.js.map +1 -0
  36. package/dist/docs-sync.js +1 -0
  37. package/dist/docs-sync.js.map +1 -0
  38. package/dist/doctor.js +1 -0
  39. package/dist/doctor.js.map +1 -0
  40. package/dist/file-delete.js +1 -0
  41. package/dist/file-delete.js.map +1 -0
  42. package/dist/file-edit.js +1 -0
  43. package/dist/file-edit.js.map +1 -0
  44. package/dist/file-read.js +1 -0
  45. package/dist/file-read.js.map +1 -0
  46. package/dist/file-write.js +1 -0
  47. package/dist/file-write.js.map +1 -0
  48. package/dist/flow-bottlenecks.js +1 -0
  49. package/dist/flow-bottlenecks.js.map +1 -0
  50. package/dist/flow-report.js +1 -0
  51. package/dist/flow-report.js.map +1 -0
  52. package/dist/gates.js +32 -20
  53. package/dist/gates.js.map +1 -0
  54. package/dist/git-branch.js +1 -0
  55. package/dist/git-branch.js.map +1 -0
  56. package/dist/git-diff.js +1 -0
  57. package/dist/git-diff.js.map +1 -0
  58. package/dist/git-log.js +1 -0
  59. package/dist/git-log.js.map +1 -0
  60. package/dist/git-status.js +1 -0
  61. package/dist/git-status.js.map +1 -0
  62. package/dist/guard-locked.js +1 -0
  63. package/dist/guard-locked.js.map +1 -0
  64. package/dist/guard-main-branch.js +1 -0
  65. package/dist/guard-main-branch.js.map +1 -0
  66. package/dist/guard-worktree-commit.js +1 -0
  67. package/dist/guard-worktree-commit.js.map +1 -0
  68. package/dist/hooks/auto-checkpoint-utils.js +52 -0
  69. package/dist/hooks/auto-checkpoint-utils.js.map +1 -0
  70. package/dist/hooks/enforcement-checks.js +1 -0
  71. package/dist/hooks/enforcement-checks.js.map +1 -0
  72. package/dist/hooks/enforcement-generator.js +185 -1
  73. package/dist/hooks/enforcement-generator.js.map +1 -0
  74. package/dist/hooks/enforcement-sync.js +91 -1
  75. package/dist/hooks/enforcement-sync.js.map +1 -0
  76. package/dist/hooks/index.js +1 -0
  77. package/dist/hooks/index.js.map +1 -0
  78. package/dist/index.js +1 -0
  79. package/dist/index.js.map +1 -0
  80. package/dist/init.js +176 -36
  81. package/dist/init.js.map +1 -0
  82. package/dist/initiative-add-wu.js +180 -59
  83. package/dist/initiative-add-wu.js.map +1 -0
  84. package/dist/initiative-bulk-assign-wus.js +3 -1
  85. package/dist/initiative-bulk-assign-wus.js.map +1 -0
  86. package/dist/initiative-create.js +1 -0
  87. package/dist/initiative-create.js.map +1 -0
  88. package/dist/initiative-edit.js +67 -32
  89. package/dist/initiative-edit.js.map +1 -0
  90. package/dist/initiative-list.js +1 -0
  91. package/dist/initiative-list.js.map +1 -0
  92. package/dist/initiative-plan.js +1 -0
  93. package/dist/initiative-plan.js.map +1 -0
  94. package/dist/initiative-remove-wu.js +1 -0
  95. package/dist/initiative-remove-wu.js.map +1 -0
  96. package/dist/initiative-status.js +1 -0
  97. package/dist/initiative-status.js.map +1 -0
  98. package/dist/lane-health.js +1 -0
  99. package/dist/lane-health.js.map +1 -0
  100. package/dist/lane-suggest.js +1 -0
  101. package/dist/lane-suggest.js.map +1 -0
  102. package/dist/lumenflow-upgrade.js +1 -0
  103. package/dist/lumenflow-upgrade.js.map +1 -0
  104. package/dist/mem-checkpoint.js +1 -0
  105. package/dist/mem-checkpoint.js.map +1 -0
  106. package/dist/mem-cleanup.js +114 -1
  107. package/dist/mem-cleanup.js.map +1 -0
  108. package/dist/mem-context.js +1 -0
  109. package/dist/mem-context.js.map +1 -0
  110. package/dist/mem-create.js +1 -0
  111. package/dist/mem-create.js.map +1 -0
  112. package/dist/mem-delete.js +1 -0
  113. package/dist/mem-delete.js.map +1 -0
  114. package/dist/mem-export.js +1 -0
  115. package/dist/mem-export.js.map +1 -0
  116. package/dist/mem-inbox.js +1 -0
  117. package/dist/mem-inbox.js.map +1 -0
  118. package/dist/mem-index.js +1 -0
  119. package/dist/mem-index.js.map +1 -0
  120. package/dist/mem-init.js +1 -0
  121. package/dist/mem-init.js.map +1 -0
  122. package/dist/mem-profile.js +1 -0
  123. package/dist/mem-profile.js.map +1 -0
  124. package/dist/mem-promote.js +1 -0
  125. package/dist/mem-promote.js.map +1 -0
  126. package/dist/mem-ready.js +1 -0
  127. package/dist/mem-ready.js.map +1 -0
  128. package/dist/mem-recover.js +1 -0
  129. package/dist/mem-recover.js.map +1 -0
  130. package/dist/mem-signal.js +12 -1
  131. package/dist/mem-signal.js.map +1 -0
  132. package/dist/mem-start.js +1 -0
  133. package/dist/mem-start.js.map +1 -0
  134. package/dist/mem-summarize.js +1 -0
  135. package/dist/mem-summarize.js.map +1 -0
  136. package/dist/mem-triage.js +2 -1
  137. package/dist/mem-triage.js.map +1 -0
  138. package/dist/merge-block.js +1 -0
  139. package/dist/merge-block.js.map +1 -0
  140. package/dist/metrics-cli.js +1 -0
  141. package/dist/metrics-cli.js.map +1 -0
  142. package/dist/metrics-snapshot.js +1 -0
  143. package/dist/metrics-snapshot.js.map +1 -0
  144. package/dist/onboarding-smoke-test.js +1 -0
  145. package/dist/onboarding-smoke-test.js.map +1 -0
  146. package/dist/orchestrate-init-status.js +1 -0
  147. package/dist/orchestrate-init-status.js.map +1 -0
  148. package/dist/orchestrate-initiative.js +20 -1
  149. package/dist/orchestrate-initiative.js.map +1 -0
  150. package/dist/orchestrate-monitor.js +1 -0
  151. package/dist/orchestrate-monitor.js.map +1 -0
  152. package/dist/plan-create.js +1 -0
  153. package/dist/plan-create.js.map +1 -0
  154. package/dist/plan-edit.js +1 -0
  155. package/dist/plan-edit.js.map +1 -0
  156. package/dist/plan-link.js +1 -0
  157. package/dist/plan-link.js.map +1 -0
  158. package/dist/plan-promote.js +1 -0
  159. package/dist/plan-promote.js.map +1 -0
  160. package/dist/public-manifest.js +773 -0
  161. package/dist/public-manifest.js.map +1 -0
  162. package/dist/release.js +3 -2
  163. package/dist/release.js.map +1 -0
  164. package/dist/rotate-progress.js +2 -1
  165. package/dist/rotate-progress.js.map +1 -0
  166. package/dist/session-coordinator.js +1 -0
  167. package/dist/session-coordinator.js.map +1 -0
  168. package/dist/shared-validators.js +78 -0
  169. package/dist/shared-validators.js.map +1 -0
  170. package/dist/signal-cleanup.js +1 -0
  171. package/dist/signal-cleanup.js.map +1 -0
  172. package/dist/spawn-list.js +1 -0
  173. package/dist/spawn-list.js.map +1 -0
  174. package/dist/state-bootstrap.js +1 -0
  175. package/dist/state-bootstrap.js.map +1 -0
  176. package/dist/state-cleanup.js +1 -0
  177. package/dist/state-cleanup.js.map +1 -0
  178. package/dist/state-doctor-fix.js +37 -1
  179. package/dist/state-doctor-fix.js.map +1 -0
  180. package/dist/state-doctor.js +11 -6
  181. package/dist/state-doctor.js.map +1 -0
  182. package/dist/sync-templates.js +1 -0
  183. package/dist/sync-templates.js.map +1 -0
  184. package/dist/trace-gen.js +1 -0
  185. package/dist/trace-gen.js.map +1 -0
  186. package/dist/validate-agent-skills.js +1 -0
  187. package/dist/validate-agent-skills.js.map +1 -0
  188. package/dist/validate-agent-sync.js +1 -0
  189. package/dist/validate-agent-sync.js.map +1 -0
  190. package/dist/validate-backlog-sync.js +1 -0
  191. package/dist/validate-backlog-sync.js.map +1 -0
  192. package/dist/validate-skills-spec.js +1 -0
  193. package/dist/validate-skills-spec.js.map +1 -0
  194. package/dist/validate.js +1 -0
  195. package/dist/validate.js.map +1 -0
  196. package/dist/wu-block.js +1 -0
  197. package/dist/wu-block.js.map +1 -0
  198. package/dist/wu-claim-repair-guidance.js +10 -0
  199. package/dist/wu-claim-repair-guidance.js.map +1 -0
  200. package/dist/wu-claim.js +40 -0
  201. package/dist/wu-claim.js.map +1 -0
  202. package/dist/wu-cleanup.js +2 -1
  203. package/dist/wu-cleanup.js.map +1 -0
  204. package/dist/wu-create.js +91 -25
  205. package/dist/wu-create.js.map +1 -0
  206. package/dist/wu-delete.js +3 -2
  207. package/dist/wu-delete.js.map +1 -0
  208. package/dist/wu-deps.js +2 -1
  209. package/dist/wu-deps.js.map +1 -0
  210. package/dist/wu-done-auto-cleanup.js +1 -0
  211. package/dist/wu-done-auto-cleanup.js.map +1 -0
  212. package/dist/wu-done-check.js +1 -0
  213. package/dist/wu-done-check.js.map +1 -0
  214. package/dist/wu-done-decay.js +88 -0
  215. package/dist/wu-done-decay.js.map +1 -0
  216. package/dist/wu-done.js +75 -18
  217. package/dist/wu-done.js.map +1 -0
  218. package/dist/wu-edit.js +2 -1
  219. package/dist/wu-edit.js.map +1 -0
  220. package/dist/wu-infer-lane.js +1 -0
  221. package/dist/wu-infer-lane.js.map +1 -0
  222. package/dist/wu-preflight.js +1 -0
  223. package/dist/wu-preflight.js.map +1 -0
  224. package/dist/wu-prep.js +105 -9
  225. package/dist/wu-prep.js.map +1 -0
  226. package/dist/wu-proto.js +12 -9
  227. package/dist/wu-proto.js.map +1 -0
  228. package/dist/wu-prune.js +1 -0
  229. package/dist/wu-prune.js.map +1 -0
  230. package/dist/wu-recover.js +54 -2
  231. package/dist/wu-recover.js.map +1 -0
  232. package/dist/wu-release.js +2 -1
  233. package/dist/wu-release.js.map +1 -0
  234. package/dist/wu-repair.js +1 -0
  235. package/dist/wu-repair.js.map +1 -0
  236. package/dist/wu-spawn-completion.js +1 -0
  237. package/dist/wu-spawn-completion.js.map +1 -0
  238. package/dist/wu-spawn.js +3 -2
  239. package/dist/wu-spawn.js.map +1 -0
  240. package/dist/wu-status.js +1 -0
  241. package/dist/wu-status.js.map +1 -0
  242. package/dist/wu-unblock.js +1 -0
  243. package/dist/wu-unblock.js.map +1 -0
  244. package/dist/wu-unlock-lane.js +1 -0
  245. package/dist/wu-unlock-lane.js.map +1 -0
  246. package/dist/wu-validate.js +58 -9
  247. package/dist/wu-validate.js.map +1 -0
  248. package/package.json +11 -21
  249. package/templates/core/.husky/pre-commit.template +5 -5
  250. package/templates/core/.mcp.json.template +8 -0
  251. package/templates/core/LUMENFLOW.md.template +2 -2
  252. package/templates/core/ai/onboarding/agent-safety-card.md.template +6 -6
  253. package/templates/core/ai/onboarding/lumenflow-force-usage.md.template +4 -4
  254. package/templates/core/ai/onboarding/vendor-support.md.template +73 -0
  255. package/templates/core/scripts/safe-git.template +29 -0
@@ -740,4 +740,229 @@ describe('lumenflow init', () => {
740
740
  });
741
741
  });
742
742
  });
743
+ // WU-1408: safe-git and pre-commit hook scaffolding
744
+ describe('WU-1408: safe-git and pre-commit scaffolding', () => {
745
+ describe('safe-git wrapper', () => {
746
+ it('should scaffold scripts/safe-git', async () => {
747
+ const options = {
748
+ force: false,
749
+ full: true,
750
+ };
751
+ await scaffoldProject(tempDir, options);
752
+ const safeGitPath = path.join(tempDir, 'scripts', 'safe-git');
753
+ expect(fs.existsSync(safeGitPath)).toBe(true);
754
+ });
755
+ it('should make safe-git executable', async () => {
756
+ const options = {
757
+ force: false,
758
+ full: true,
759
+ };
760
+ await scaffoldProject(tempDir, options);
761
+ const safeGitPath = path.join(tempDir, 'scripts', 'safe-git');
762
+ const stats = fs.statSync(safeGitPath);
763
+ // Check for executable bit (owner, group, or other)
764
+ // eslint-disable-next-line no-bitwise
765
+ const isExecutable = (stats.mode & 0o111) !== 0;
766
+ expect(isExecutable).toBe(true);
767
+ });
768
+ it('should include worktree remove block in safe-git', async () => {
769
+ const options = {
770
+ force: false,
771
+ full: true,
772
+ };
773
+ await scaffoldProject(tempDir, options);
774
+ const safeGitPath = path.join(tempDir, 'scripts', 'safe-git');
775
+ const content = fs.readFileSync(safeGitPath, 'utf-8');
776
+ expect(content).toContain('worktree');
777
+ expect(content).toContain('remove');
778
+ expect(content).toContain('BLOCKED');
779
+ });
780
+ it('should scaffold safe-git even in minimal mode', async () => {
781
+ const options = {
782
+ force: false,
783
+ full: false, // minimal mode
784
+ };
785
+ await scaffoldProject(tempDir, options);
786
+ const safeGitPath = path.join(tempDir, 'scripts', 'safe-git');
787
+ expect(fs.existsSync(safeGitPath)).toBe(true);
788
+ });
789
+ });
790
+ describe('pre-commit hook', () => {
791
+ it('should scaffold .husky/pre-commit', async () => {
792
+ const options = {
793
+ force: false,
794
+ full: true,
795
+ };
796
+ await scaffoldProject(tempDir, options);
797
+ const preCommitPath = path.join(tempDir, '.husky', 'pre-commit');
798
+ expect(fs.existsSync(preCommitPath)).toBe(true);
799
+ });
800
+ it('should make pre-commit executable', async () => {
801
+ const options = {
802
+ force: false,
803
+ full: true,
804
+ };
805
+ await scaffoldProject(tempDir, options);
806
+ const preCommitPath = path.join(tempDir, '.husky', 'pre-commit');
807
+ const stats = fs.statSync(preCommitPath);
808
+ // eslint-disable-next-line no-bitwise
809
+ const isExecutable = (stats.mode & 0o111) !== 0;
810
+ expect(isExecutable).toBe(true);
811
+ });
812
+ it('should NOT run pnpm test in pre-commit hook', async () => {
813
+ const options = {
814
+ force: false,
815
+ full: true,
816
+ };
817
+ await scaffoldProject(tempDir, options);
818
+ const preCommitPath = path.join(tempDir, '.husky', 'pre-commit');
819
+ const content = fs.readFileSync(preCommitPath, 'utf-8');
820
+ // The pre-commit hook should NOT assume pnpm test exists
821
+ expect(content).not.toContain('pnpm test');
822
+ expect(content).not.toContain('npm test');
823
+ });
824
+ it('should block commits to main/master in pre-commit', async () => {
825
+ const options = {
826
+ force: false,
827
+ full: true,
828
+ };
829
+ await scaffoldProject(tempDir, options);
830
+ const preCommitPath = path.join(tempDir, '.husky', 'pre-commit');
831
+ const content = fs.readFileSync(preCommitPath, 'utf-8');
832
+ // Should protect main branch
833
+ expect(content).toContain('main');
834
+ expect(content).toContain('BLOCK');
835
+ });
836
+ it('should scaffold pre-commit even in minimal mode', async () => {
837
+ const options = {
838
+ force: false,
839
+ full: false, // minimal mode
840
+ };
841
+ await scaffoldProject(tempDir, options);
842
+ const preCommitPath = path.join(tempDir, '.husky', 'pre-commit');
843
+ expect(fs.existsSync(preCommitPath)).toBe(true);
844
+ });
845
+ });
846
+ });
847
+ // WU-1413: MCP server configuration scaffolding
848
+ describe('WU-1413: .mcp.json scaffolding', () => {
849
+ const MCP_JSON_FILE = '.mcp.json';
850
+ describe('.mcp.json creation with --client claude', () => {
851
+ it('should scaffold .mcp.json when --client claude is used', async () => {
852
+ const options = {
853
+ force: false,
854
+ full: false,
855
+ client: 'claude',
856
+ };
857
+ await scaffoldProject(tempDir, options);
858
+ const mcpJsonPath = path.join(tempDir, MCP_JSON_FILE);
859
+ expect(fs.existsSync(mcpJsonPath)).toBe(true);
860
+ });
861
+ it('should include lumenflow MCP server configuration', async () => {
862
+ const options = {
863
+ force: false,
864
+ full: false,
865
+ client: 'claude',
866
+ };
867
+ await scaffoldProject(tempDir, options);
868
+ const mcpJsonPath = path.join(tempDir, MCP_JSON_FILE);
869
+ const content = fs.readFileSync(mcpJsonPath, 'utf-8');
870
+ const mcpConfig = JSON.parse(content);
871
+ // Should have mcpServers key
872
+ expect(mcpConfig.mcpServers).toBeDefined();
873
+ // Should have lumenflow server entry
874
+ expect(mcpConfig.mcpServers.lumenflow).toBeDefined();
875
+ // Should use npx command
876
+ expect(mcpConfig.mcpServers.lumenflow.command).toBe('npx');
877
+ // Should reference @lumenflow/mcp package
878
+ expect(mcpConfig.mcpServers.lumenflow.args).toContain('@lumenflow/mcp');
879
+ });
880
+ it('should be valid JSON', async () => {
881
+ const options = {
882
+ force: false,
883
+ full: false,
884
+ client: 'claude',
885
+ };
886
+ await scaffoldProject(tempDir, options);
887
+ const mcpJsonPath = path.join(tempDir, MCP_JSON_FILE);
888
+ const content = fs.readFileSync(mcpJsonPath, 'utf-8');
889
+ // Should parse without error
890
+ expect(() => JSON.parse(content)).not.toThrow();
891
+ });
892
+ });
893
+ describe('.mcp.json creation with --client all', () => {
894
+ it('should scaffold .mcp.json when --client all is used', async () => {
895
+ const options = {
896
+ force: false,
897
+ full: false,
898
+ client: 'all',
899
+ };
900
+ await scaffoldProject(tempDir, options);
901
+ const mcpJsonPath = path.join(tempDir, MCP_JSON_FILE);
902
+ expect(fs.existsSync(mcpJsonPath)).toBe(true);
903
+ });
904
+ });
905
+ describe('.mcp.json NOT created with other clients', () => {
906
+ it('should NOT scaffold .mcp.json when --client none is used', async () => {
907
+ const options = {
908
+ force: false,
909
+ full: false,
910
+ client: 'none',
911
+ };
912
+ await scaffoldProject(tempDir, options);
913
+ const mcpJsonPath = path.join(tempDir, MCP_JSON_FILE);
914
+ expect(fs.existsSync(mcpJsonPath)).toBe(false);
915
+ });
916
+ it('should NOT scaffold .mcp.json when --client cursor is used', async () => {
917
+ const options = {
918
+ force: false,
919
+ full: false,
920
+ client: 'cursor',
921
+ };
922
+ await scaffoldProject(tempDir, options);
923
+ const mcpJsonPath = path.join(tempDir, MCP_JSON_FILE);
924
+ expect(fs.existsSync(mcpJsonPath)).toBe(false);
925
+ });
926
+ it('should NOT scaffold .mcp.json when no client is specified', async () => {
927
+ const options = {
928
+ force: false,
929
+ full: false,
930
+ };
931
+ await scaffoldProject(tempDir, options);
932
+ const mcpJsonPath = path.join(tempDir, MCP_JSON_FILE);
933
+ expect(fs.existsSync(mcpJsonPath)).toBe(false);
934
+ });
935
+ });
936
+ describe('.mcp.json file modes', () => {
937
+ it('should skip .mcp.json if it already exists (skip mode)', async () => {
938
+ // Create existing .mcp.json
939
+ const existingContent = '{"mcpServers":{"custom":{}}}';
940
+ fs.writeFileSync(path.join(tempDir, MCP_JSON_FILE), existingContent);
941
+ const options = {
942
+ force: false,
943
+ full: false,
944
+ client: 'claude',
945
+ };
946
+ const result = await scaffoldProject(tempDir, options);
947
+ expect(result.skipped).toContain(MCP_JSON_FILE);
948
+ // Content should not be changed
949
+ const content = fs.readFileSync(path.join(tempDir, MCP_JSON_FILE), 'utf-8');
950
+ expect(content).toBe(existingContent);
951
+ });
952
+ it('should overwrite .mcp.json in force mode', async () => {
953
+ // Create existing .mcp.json
954
+ fs.writeFileSync(path.join(tempDir, MCP_JSON_FILE), '{"custom":true}');
955
+ const options = {
956
+ force: true,
957
+ full: false,
958
+ client: 'claude',
959
+ };
960
+ const result = await scaffoldProject(tempDir, options);
961
+ expect(result.created).toContain(MCP_JSON_FILE);
962
+ const content = fs.readFileSync(path.join(tempDir, MCP_JSON_FILE), 'utf-8');
963
+ const mcpConfig = JSON.parse(content);
964
+ expect(mcpConfig.mcpServers.lumenflow).toBeDefined();
965
+ });
966
+ });
967
+ });
743
968
  });
@@ -10,7 +10,8 @@ import { describe, it, expect, vi, beforeEach, afterEach, beforeAll } from 'vite
10
10
  import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
11
11
  import { join } from 'node:path';
12
12
  import { tmpdir } from 'node:os';
13
- import { stringifyYAML } from '@lumenflow/core/dist/wu-yaml.js';
13
+ import { stringifyYAML, readWU } from '@lumenflow/core/dist/wu-yaml.js';
14
+ import { readInitiative } from '@lumenflow/initiatives/dist/initiative-yaml.js';
14
15
  // Test constants to avoid lint warnings about duplicate strings
15
16
  const TEST_WU_ID = 'WU-123';
16
17
  const TEST_INIT_ID = 'INIT-001';
@@ -22,6 +23,7 @@ const TEST_INIT_TITLE = 'Test Initiative';
22
23
  const TEST_INIT_STATUS = 'open';
23
24
  const TEST_DATE = '2026-01-25';
24
25
  const MIN_DESCRIPTION_LENGTH = 50;
26
+ const TEST_WU_ID_2 = 'WU-124';
25
27
  // Valid WU document template
26
28
  const createValidWUDoc = (overrides = {}) => ({
27
29
  id: TEST_WU_ID,
@@ -324,6 +326,50 @@ describe('initiative:add-wu WU validation (WU-1330)', () => {
324
326
  expect(result.valid).toBe(true);
325
327
  });
326
328
  });
329
+ describe('batch linking (WU-1460)', () => {
330
+ it('should normalize repeatable --wu values with dedupe and order preservation', async () => {
331
+ const { normalizeWuIds } = await import('../initiative-add-wu.js');
332
+ expect(normalizeWuIds(TEST_WU_ID)).toEqual([TEST_WU_ID]);
333
+ expect(normalizeWuIds([TEST_WU_ID, TEST_WU_ID_2, TEST_WU_ID])).toEqual([
334
+ TEST_WU_ID,
335
+ TEST_WU_ID_2,
336
+ ]);
337
+ });
338
+ it('should update multiple WUs and initiative in one execute call', async () => {
339
+ const { buildAddWuMicroWorktreeOptions } = await import('../initiative-add-wu.js');
340
+ // Setup valid WUs and initiative
341
+ const wuDir = join(tempDir, WU_REL_PATH);
342
+ const initDir = join(tempDir, INIT_REL_PATH);
343
+ mkdirSync(wuDir, { recursive: true });
344
+ mkdirSync(initDir, { recursive: true });
345
+ const wuPath1 = join(wuDir, `${TEST_WU_ID}.yaml`);
346
+ const wuPath2 = join(wuDir, `${TEST_WU_ID_2}.yaml`);
347
+ const initPath = join(initDir, `${TEST_INIT_ID}.yaml`);
348
+ writeFileSync(wuPath1, stringifyYAML(createValidWUDoc({ id: TEST_WU_ID })));
349
+ writeFileSync(wuPath2, stringifyYAML(createValidWUDoc({ id: TEST_WU_ID_2 })));
350
+ writeFileSync(initPath, stringifyYAML(createValidInitDoc()));
351
+ process.chdir(tempDir);
352
+ const options = buildAddWuMicroWorktreeOptions([TEST_WU_ID, TEST_WU_ID_2], TEST_INIT_ID);
353
+ const result = await options.execute({ worktreePath: tempDir });
354
+ expect(result.files).toContain(`${WU_REL_PATH}/${TEST_WU_ID}.yaml`);
355
+ expect(result.files).toContain(`${WU_REL_PATH}/${TEST_WU_ID_2}.yaml`);
356
+ expect(result.files).toContain(`${INIT_REL_PATH}/${TEST_INIT_ID}.yaml`);
357
+ const updatedWu1 = readWU(wuPath1, TEST_WU_ID);
358
+ const updatedWu2 = readWU(wuPath2, TEST_WU_ID_2);
359
+ const updatedInit = readInitiative(initPath, TEST_INIT_ID);
360
+ expect(updatedWu1.initiative).toBe(TEST_INIT_ID);
361
+ expect(updatedWu2.initiative).toBe(TEST_INIT_ID);
362
+ expect(updatedInit.wus).toContain(TEST_WU_ID);
363
+ expect(updatedInit.wus).toContain(TEST_WU_ID_2);
364
+ });
365
+ it('should validate conflicting links across multiple WUs', async () => {
366
+ const { validateNoConflictingLinks } = await import('../initiative-add-wu.js');
367
+ expect(() => validateNoConflictingLinks([
368
+ { id: TEST_WU_ID, initiative: TEST_INIT_ID },
369
+ { id: TEST_WU_ID_2, initiative: 'INIT-999' },
370
+ ], TEST_INIT_ID)).toThrow();
371
+ });
372
+ });
327
373
  describe('error formatting', () => {
328
374
  it('should format errors in human-readable format', async () => {
329
375
  const { formatValidationErrors } = await import('../initiative-add-wu.js');
@@ -356,6 +402,24 @@ describe('initiative:add-wu WU validation (WU-1330)', () => {
356
402
  const mod = await import('../initiative-add-wu.js');
357
403
  expect(typeof mod.formatRetryExhaustionError).toBe('function');
358
404
  });
405
+ it('should export operation-level push retry override (WU-1459)', async () => {
406
+ const mod = await import('../initiative-add-wu.js');
407
+ expect(mod.INITIATIVE_ADD_WU_PUSH_RETRY_OVERRIDE).toBeDefined();
408
+ expect(mod.INITIATIVE_ADD_WU_PUSH_RETRY_OVERRIDE.retries).toBeGreaterThan(3);
409
+ expect(mod.INITIATIVE_ADD_WU_PUSH_RETRY_OVERRIDE.min_delay_ms).toBeGreaterThan(100);
410
+ });
411
+ it('should export helper to build micro-worktree options (WU-1459)', async () => {
412
+ const mod = await import('../initiative-add-wu.js');
413
+ expect(typeof mod.buildAddWuMicroWorktreeOptions).toBe('function');
414
+ const options = mod.buildAddWuMicroWorktreeOptions(TEST_WU_ID, TEST_INIT_ID);
415
+ expect(options.pushOnly).toBe(true);
416
+ expect(options.pushRetryOverride).toEqual(mod.INITIATIVE_ADD_WU_PUSH_RETRY_OVERRIDE);
417
+ });
418
+ it('should export batch helpers (WU-1460)', async () => {
419
+ const mod = await import('../initiative-add-wu.js');
420
+ expect(typeof mod.normalizeWuIds).toBe('function');
421
+ expect(typeof mod.validateNoConflictingLinks).toBe('function');
422
+ });
359
423
  });
360
424
  });
361
425
  /**
@@ -416,5 +480,11 @@ describe('initiative:add-wu retry handling (WU-1333)', () => {
416
480
  // Should mention concurrent agents as possible cause
417
481
  expect(formatted).toMatch(/concurrent|agent|traffic/i);
418
482
  });
483
+ it('should include git.push_retry tuning guidance', async () => {
484
+ const { formatRetryExhaustionError } = await import('../initiative-add-wu.js');
485
+ const retryError = new Error('Push failed after 3 attempts.');
486
+ const formatted = formatRetryExhaustionError(retryError, TEST_WU_ID, TEST_INIT_ID);
487
+ expect(formatted).toContain('git.push_retry.retries');
488
+ });
419
489
  });
420
490
  });
@@ -0,0 +1,30 @@
1
+ /**
2
+ * @file no-beacon-references-docs.test.ts
3
+ * Guardrail test: public + onboarding docs must not reference legacy `.beacon` paths (WU-1450).
4
+ */
5
+ import { describe, it, expect } from 'vitest';
6
+ import { readFileSync } from 'node:fs';
7
+ import path from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+ function repoRootFromThisFile() {
10
+ // packages/@lumenflow/cli/src/__tests__/no-beacon-references-docs.test.ts -> repo root
11
+ const thisDir = path.dirname(fileURLToPath(import.meta.url));
12
+ return path.resolve(thisDir, '..', '..', '..', '..', '..');
13
+ }
14
+ describe('no legacy .beacon references in docs (WU-1450)', () => {
15
+ it('should not contain .beacon references in onboarding/public docs', () => {
16
+ const repoRoot = repoRootFromThisFile();
17
+ const files = [
18
+ 'docs/04-operations/_frameworks/lumenflow/agent/onboarding/agent-safety-card.md',
19
+ 'docs/04-operations/_frameworks/lumenflow/agent/onboarding/lumenflow-force-usage.md',
20
+ 'apps/docs/src/content/docs/getting-started/upgrade.mdx',
21
+ 'apps/docs/src/content/docs/reference/changelog.mdx',
22
+ 'apps/docs/src/content/docs/reference/compatibility.mdx',
23
+ ];
24
+ for (const relPath of files) {
25
+ const absPath = path.join(repoRoot, relPath);
26
+ const content = readFileSync(absPath, 'utf-8');
27
+ expect(content, `${relPath} should not reference .beacon`).not.toContain('.beacon');
28
+ }
29
+ });
30
+ });
@@ -0,0 +1,39 @@
1
+ /**
2
+ * @file no-beacon-references.test.ts
3
+ * Guardrail test: `.beacon` is legacy and must not be referenced in docs/templates/scripts (WU-1447).
4
+ *
5
+ * This keeps onboarding friction-free by ensuring `.lumenflow/` is the single canonical namespace.
6
+ */
7
+ import { describe, it, expect } from 'vitest';
8
+ import { readFileSync } from 'node:fs';
9
+ import path from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
11
+ function repoRootFromThisFile() {
12
+ // packages/@lumenflow/cli/src/__tests__/no-beacon-references.test.ts -> repo root (../../../../../)
13
+ const thisDir = path.dirname(fileURLToPath(import.meta.url));
14
+ return path.resolve(thisDir, '..', '..', '..', '..', '..');
15
+ }
16
+ describe('no legacy .beacon references (WU-1447)', () => {
17
+ it('should not contain .beacon references in tracked onboarding docs/templates/scripts', () => {
18
+ const repoRoot = repoRootFromThisFile();
19
+ const files = [
20
+ 'scripts/safe-git',
21
+ 'scripts/hooks/check-lockfile.sh',
22
+ 'scripts/hooks/scan-secrets.sh',
23
+ 'scripts/hooks/validate-paths.sh',
24
+ 'scripts/hooks/validate-worktree-discipline.sh',
25
+ 'LUMENFLOW.md',
26
+ 'packages/@lumenflow/cli/templates/core/LUMENFLOW.md.template',
27
+ 'packages/@lumenflow/cli/templates/core/ai/onboarding/agent-safety-card.md.template',
28
+ 'packages/@lumenflow/cli/templates/core/ai/onboarding/lumenflow-force-usage.md.template',
29
+ 'apps/docs/src/content/docs/guides/agent-onboarding.mdx',
30
+ 'apps/docs/src/content/docs/guides/ai-agents.mdx',
31
+ 'packages/@lumenflow/agent/README.md',
32
+ ];
33
+ for (const relPath of files) {
34
+ const absPath = path.join(repoRoot, relPath);
35
+ const content = readFileSync(absPath, 'utf-8');
36
+ expect(content, `${relPath} should not reference .beacon`).not.toContain('.beacon');
37
+ }
38
+ });
39
+ });
@@ -13,7 +13,7 @@ const USER_NAME_CONFIG = 'user.name';
13
13
  const TEST_EMAIL = 'test@test.com';
14
14
  const TEST_USERNAME = 'Test';
15
15
  const FORCE_BYPASSES_LOG = 'force-bypasses.log';
16
- // Create a temporary directory for testing to avoid polluting the real .beacon directory
16
+ // Create a temporary directory for testing to avoid polluting the real .lumenflow directory
17
17
  const createTempDir = () => {
18
18
  return fs.mkdtempSync(path.join(os.tmpdir(), 'safe-git-test-'));
19
19
  };
@@ -128,7 +128,7 @@ describe('safe-git', () => {
128
128
  env: { ...process.env, LUMENFLOW_FORCE: '1' },
129
129
  });
130
130
  // Check that the force bypass log exists and contains the entry
131
- const bypassLog = path.join(testRepo, '.beacon', FORCE_BYPASSES_LOG);
131
+ const bypassLog = path.join(testRepo, '.lumenflow', FORCE_BYPASSES_LOG);
132
132
  expect(fs.existsSync(bypassLog)).toBe(true);
133
133
  const logContent = fs.readFileSync(bypassLog, 'utf-8');
134
134
  expect(logContent).toContain('reset --hard');
@@ -155,7 +155,7 @@ describe('safe-git', () => {
155
155
  encoding: 'utf-8',
156
156
  env: { ...process.env, LUMENFLOW_FORCE: '1', LUMENFLOW_FORCE_REASON: testReason },
157
157
  });
158
- const bypassLog = path.join(testRepo, '.beacon', FORCE_BYPASSES_LOG);
158
+ const bypassLog = path.join(testRepo, '.lumenflow', FORCE_BYPASSES_LOG);
159
159
  const logContent = fs.readFileSync(bypassLog, 'utf-8');
160
160
  expect(logContent).toContain(testReason);
161
161
  });
@@ -182,7 +182,7 @@ describe('safe-git', () => {
182
182
  stdio: ['pipe', 'pipe', 'pipe'],
183
183
  });
184
184
  // Check the bypasslog for the NO_REASON marker
185
- const bypassLog = path.join(testRepo, '.beacon', FORCE_BYPASSES_LOG);
185
+ const bypassLog = path.join(testRepo, '.lumenflow', FORCE_BYPASSES_LOG);
186
186
  const logContent = fs.readFileSync(bypassLog, 'utf-8');
187
187
  expect(logContent).toContain('NO_REASON');
188
188
  });
@@ -82,6 +82,28 @@ describe('wu:create required field aggregation (WU-1366)', () => {
82
82
  expect(result.valid).toBe(false);
83
83
  expect(result.errors.some((e) => e.includes('--spec-refs'))).toBe(true);
84
84
  });
85
+ it('should treat empty spec-refs array as missing for feature WUs', () => {
86
+ const result = validateCreateSpec({
87
+ id: TEST_WU_ID,
88
+ lane: TEST_LANE,
89
+ title: 'Test WU',
90
+ priority: 'P2',
91
+ type: 'feature',
92
+ opts: {
93
+ description: VALID_DESCRIPTION,
94
+ acceptance: TEST_ACCEPTANCE,
95
+ exposure: 'backend-only',
96
+ codePaths: ['packages/@lumenflow/cli/src/wu-create.ts'],
97
+ testPathsUnit: [
98
+ 'packages/@lumenflow/cli/src/__tests__/wu-create-required-fields.test.ts',
99
+ ],
100
+ specRefs: [],
101
+ strict: false,
102
+ },
103
+ });
104
+ expect(result.valid).toBe(false);
105
+ expect(result.errors.some((e) => e.includes('--spec-refs'))).toBe(true);
106
+ });
85
107
  it('should return all errors at once, not fail on first error', () => {
86
108
  const result = validateCreateSpec({
87
109
  id: TEST_WU_ID,
@@ -0,0 +1,121 @@
1
+ /**
2
+ * @file wu-create.test.ts
3
+ * Tests for wu:create helpers and warnings (WU-1429)
4
+ */
5
+ import { describe, it, expect } from 'vitest';
6
+ import { buildWUContent, collectInitiativeWarnings, validateCreateSpec } from '../wu-create.js';
7
+ const BASE_WU = {
8
+ id: 'WU-1429',
9
+ lane: 'Framework: CLI',
10
+ title: 'Test WU',
11
+ priority: 'P2',
12
+ type: 'feature',
13
+ created: '2026-02-04',
14
+ opts: {
15
+ description: 'Context: test context.\nProblem: test problem.\nSolution: test solution that exceeds minimum.',
16
+ acceptance: ['Acceptance criterion'],
17
+ exposure: 'backend-only',
18
+ codePaths: ['packages/@lumenflow/cli/src/wu-create.ts'],
19
+ testPathsUnit: ['packages/@lumenflow/cli/src/__tests__/wu-create.test.ts'],
20
+ specRefs: ['lumenflow://plans/WU-1429-plan.md'],
21
+ },
22
+ };
23
+ describe('wu:create helpers (WU-1429)', () => {
24
+ it('should default notes to non-empty placeholder when not provided', () => {
25
+ const wu = buildWUContent({
26
+ ...BASE_WU,
27
+ opts: {
28
+ ...BASE_WU.opts,
29
+ // Intentionally omit notes
30
+ notes: undefined,
31
+ },
32
+ });
33
+ expect(typeof wu.notes).toBe('string');
34
+ expect(wu.notes.trim().length).toBeGreaterThan(0);
35
+ expect(wu.notes).toContain('(auto)');
36
+ });
37
+ it('should persist notes when provided', () => {
38
+ const wu = buildWUContent({
39
+ ...BASE_WU,
40
+ opts: {
41
+ ...BASE_WU.opts,
42
+ notes: 'Implementation notes for test',
43
+ },
44
+ });
45
+ expect(wu.notes).toBe('Implementation notes for test');
46
+ });
47
+ it('should allow creating a plan-first WU without explicit test flags when code_paths are non-code', () => {
48
+ const validation = validateCreateSpec({
49
+ id: 'WU-2000',
50
+ lane: 'Framework: CLI',
51
+ title: 'Plan-only spec creation',
52
+ priority: 'P2',
53
+ type: 'feature',
54
+ opts: {
55
+ description: 'Context: test context.\nProblem: test problem.\nSolution: test solution that exceeds minimum.',
56
+ acceptance: ['Acceptance criterion'],
57
+ exposure: 'backend-only',
58
+ // Non-code file path: manual-only tests are acceptable.
59
+ codePaths: ['docs/README.md'],
60
+ // No testPathsManual/unit/e2e provided - should auto-default manual stub.
61
+ specRefs: ['lumenflow://plans/WU-2000-plan.md'],
62
+ strict: false,
63
+ },
64
+ });
65
+ expect(validation.valid).toBe(true);
66
+ const wu = buildWUContent({
67
+ id: 'WU-2000',
68
+ lane: 'Framework: CLI',
69
+ title: 'Plan-only spec creation',
70
+ priority: 'P2',
71
+ type: 'feature',
72
+ created: '2026-02-05',
73
+ opts: {
74
+ description: 'Context: test context.\nProblem: test problem.\nSolution: test solution that exceeds minimum.',
75
+ acceptance: ['Acceptance criterion'],
76
+ exposure: 'backend-only',
77
+ codePaths: ['docs/README.md'],
78
+ specRefs: ['lumenflow://plans/WU-2000-plan.md'],
79
+ },
80
+ });
81
+ expect(wu.tests?.manual?.length).toBeGreaterThan(0);
82
+ });
83
+ it('should warn when initiative has phases but no --phase is provided', () => {
84
+ const warnings = collectInitiativeWarnings({
85
+ initiativeId: 'INIT-TEST',
86
+ initiativeDoc: {
87
+ phases: [{ id: 1, title: 'Phase 1' }],
88
+ },
89
+ phase: undefined,
90
+ specRefs: ['lumenflow://plans/WU-1429-plan.md'],
91
+ });
92
+ expect(warnings).toEqual(expect.arrayContaining([
93
+ 'Initiative INIT-TEST has phases defined. Consider adding --phase to link this WU to a phase.',
94
+ ]));
95
+ });
96
+ it('should warn when initiative has related_plan but no spec_refs', () => {
97
+ const warnings = collectInitiativeWarnings({
98
+ initiativeId: 'INIT-TEST',
99
+ initiativeDoc: {
100
+ related_plan: 'lumenflow://plans/INIT-TEST-plan.md',
101
+ },
102
+ phase: '1',
103
+ specRefs: [],
104
+ });
105
+ expect(warnings).toEqual(expect.arrayContaining([
106
+ 'Initiative INIT-TEST has related_plan (lumenflow://plans/INIT-TEST-plan.md). Consider adding --spec-refs to link this WU to the plan.',
107
+ ]));
108
+ });
109
+ it('should not warn when phase and spec_refs are provided', () => {
110
+ const warnings = collectInitiativeWarnings({
111
+ initiativeId: 'INIT-TEST',
112
+ initiativeDoc: {
113
+ phases: [{ id: 1, title: 'Phase 1' }],
114
+ related_plan: 'lumenflow://plans/INIT-TEST-plan.md',
115
+ },
116
+ phase: '1',
117
+ specRefs: ['lumenflow://plans/WU-1429-plan.md'],
118
+ });
119
+ expect(warnings).toEqual([]);
120
+ });
121
+ });
@@ -0,0 +1,20 @@
1
+ /**
2
+ * @file wu-done-docs-only-policy.test.ts
3
+ * Guardrail test: docs-only eligibility checks must not use raw type/exposure string literals (WU-1446).
4
+ *
5
+ * This keeps CLI policy logic DRY and aligned with core constants/helpers.
6
+ */
7
+ import { describe, it, expect } from 'vitest';
8
+ import { readFileSync } from 'node:fs';
9
+ import path from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
11
+ describe('wu:done docs-only policy (WU-1446)', () => {
12
+ it('should not use raw documentation string comparisons for type/exposure checks', () => {
13
+ const thisDir = path.dirname(fileURLToPath(import.meta.url));
14
+ const filePath = path.join(thisDir, '..', 'wu-done.ts');
15
+ const content = readFileSync(filePath, 'utf-8');
16
+ // These comparisons should use core constants/helpers instead.
17
+ expect(content).not.toContain("exposure === 'documentation'");
18
+ expect(content).not.toContain("type === 'documentation'");
19
+ });
20
+ });
@@ -0,0 +1,35 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ vi.mock('node:child_process', async (importOriginal) => {
3
+ const actual = await importOriginal();
4
+ return { ...actual, spawnSync: vi.fn() };
5
+ });
6
+ vi.mock('node:fs', async (importOriginal) => {
7
+ const actual = await importOriginal();
8
+ return { ...actual, existsSync: vi.fn() };
9
+ });
10
+ describe('wu-prep default exec helpers (WU-1441)', () => {
11
+ beforeEach(() => {
12
+ vi.resetModules();
13
+ vi.resetAllMocks();
14
+ });
15
+ it('uses node + dist wu-validate for JSON comparison when available', async () => {
16
+ const { spawnSync } = await import('node:child_process');
17
+ const { existsSync } = await import('node:fs');
18
+ // Pretend dist sibling exists so defaultExec picks node+dist path (not pnpm on main).
19
+ vi.mocked(existsSync).mockReturnValue(true);
20
+ // Both worktree and main should report WU-1 invalid.
21
+ vi.mocked(spawnSync).mockReturnValue({
22
+ status: 1,
23
+ stdout: JSON.stringify({ invalid: [{ wuId: 'WU-1' }] }),
24
+ stderr: '',
25
+ });
26
+ const { checkPreExistingFailures } = await import('../wu-prep.js');
27
+ const result = await checkPreExistingFailures({ mainCheckout: '/repo' });
28
+ expect(result.error).toBeUndefined();
29
+ expect(result.hasPreExisting).toBe(true);
30
+ expect(result.hasNewFailures).toBe(false);
31
+ // Default exec should run node directly, not "pnpm wu:validate" from the main checkout.
32
+ expect(vi.mocked(spawnSync).mock.calls.length).toBeGreaterThan(0);
33
+ expect(vi.mocked(spawnSync).mock.calls[0]?.[0]).toBe('node');
34
+ });
35
+ });