@lumenflow/cli 2.7.0 → 2.9.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 (84) hide show
  1. package/README.md +121 -105
  2. package/dist/__tests__/agent-spawn-coordination.test.js +451 -0
  3. package/dist/__tests__/commands/integrate.test.js +165 -0
  4. package/dist/__tests__/commands.test.js +75 -0
  5. package/dist/__tests__/doctor.test.js +510 -0
  6. package/dist/__tests__/gates-config.test.js +0 -1
  7. package/dist/__tests__/hooks/enforcement.test.js +279 -0
  8. package/dist/__tests__/init-greenfield.test.js +247 -0
  9. package/dist/__tests__/init-quick-ref.test.js +0 -1
  10. package/dist/__tests__/init-template-portability.test.js +0 -1
  11. package/dist/__tests__/init.test.js +249 -0
  12. package/dist/__tests__/initiative-e2e.test.js +442 -0
  13. package/dist/__tests__/initiative-plan-replacement.test.js +0 -1
  14. package/dist/__tests__/memory-integration.test.js +333 -0
  15. package/dist/__tests__/release.test.js +1 -1
  16. package/dist/__tests__/safe-git.test.js +0 -1
  17. package/dist/__tests__/state-doctor.test.js +54 -0
  18. package/dist/__tests__/sync-templates.test.js +255 -0
  19. package/dist/__tests__/wu-create-required-fields.test.js +121 -0
  20. package/dist/__tests__/wu-done-auto-cleanup.test.js +135 -0
  21. package/dist/__tests__/wu-lifecycle-integration.test.js +388 -0
  22. package/dist/backlog-prune.js +0 -1
  23. package/dist/cli-entry-point.js +0 -1
  24. package/dist/commands/integrate.js +229 -0
  25. package/dist/commands.js +171 -0
  26. package/dist/docs-sync.js +46 -0
  27. package/dist/doctor.js +479 -10
  28. package/dist/gates.js +0 -7
  29. package/dist/hooks/enforcement-checks.js +209 -0
  30. package/dist/hooks/enforcement-generator.js +365 -0
  31. package/dist/hooks/enforcement-sync.js +243 -0
  32. package/dist/hooks/index.js +7 -0
  33. package/dist/init.js +502 -17
  34. package/dist/initiative-add-wu.js +0 -2
  35. package/dist/initiative-create.js +0 -3
  36. package/dist/initiative-edit.js +0 -5
  37. package/dist/initiative-plan.js +0 -1
  38. package/dist/initiative-remove-wu.js +0 -2
  39. package/dist/lane-health.js +0 -2
  40. package/dist/lane-suggest.js +0 -1
  41. package/dist/mem-checkpoint.js +0 -2
  42. package/dist/mem-cleanup.js +0 -2
  43. package/dist/mem-context.js +0 -3
  44. package/dist/mem-create.js +0 -2
  45. package/dist/mem-delete.js +0 -3
  46. package/dist/mem-inbox.js +0 -2
  47. package/dist/mem-index.js +0 -1
  48. package/dist/mem-init.js +0 -2
  49. package/dist/mem-profile.js +0 -1
  50. package/dist/mem-promote.js +0 -1
  51. package/dist/mem-ready.js +0 -2
  52. package/dist/mem-signal.js +0 -2
  53. package/dist/mem-start.js +0 -2
  54. package/dist/mem-summarize.js +0 -2
  55. package/dist/metrics-cli.js +1 -1
  56. package/dist/metrics-snapshot.js +1 -1
  57. package/dist/onboarding-smoke-test.js +0 -5
  58. package/dist/orchestrate-init-status.js +0 -1
  59. package/dist/orchestrate-initiative.js +0 -1
  60. package/dist/orchestrate-monitor.js +0 -1
  61. package/dist/plan-create.js +0 -2
  62. package/dist/plan-edit.js +0 -2
  63. package/dist/plan-link.js +0 -2
  64. package/dist/plan-promote.js +0 -2
  65. package/dist/signal-cleanup.js +0 -4
  66. package/dist/state-bootstrap.js +0 -1
  67. package/dist/state-cleanup.js +0 -4
  68. package/dist/state-doctor-fix.js +5 -8
  69. package/dist/state-doctor.js +0 -11
  70. package/dist/sync-templates.js +188 -34
  71. package/dist/wu-block.js +100 -48
  72. package/dist/wu-claim.js +1 -22
  73. package/dist/wu-cleanup.js +0 -1
  74. package/dist/wu-create.js +0 -2
  75. package/dist/wu-done-auto-cleanup.js +139 -0
  76. package/dist/wu-done.js +11 -4
  77. package/dist/wu-edit.js +0 -12
  78. package/dist/wu-preflight.js +0 -1
  79. package/dist/wu-prep.js +0 -1
  80. package/dist/wu-proto.js +0 -1
  81. package/dist/wu-spawn.js +0 -3
  82. package/dist/wu-unblock.js +0 -2
  83. package/dist/wu-validate.js +0 -1
  84. package/package.json +9 -7
@@ -0,0 +1,243 @@
1
+ /**
2
+ * @file enforcement-sync.ts
3
+ * Sync enforcement hooks based on LumenFlow configuration (WU-1367)
4
+ *
5
+ * This module handles syncing Claude Code hooks during setup when
6
+ * enforcement.hooks=true in the configuration.
7
+ */
8
+ // fs operations use runtime-provided paths from LumenFlow configuration
9
+ // Object injection sink warnings are false positives for array indexing
10
+ import * as fs from 'node:fs';
11
+ import * as path from 'node:path';
12
+ import * as yaml from 'yaml';
13
+ import { generateEnforcementHooks, generateEnforceWorktreeScript, generateRequireWuScript, generateWarnIncompleteScript, } from './enforcement-generator.js';
14
+ /**
15
+ * Read LumenFlow configuration from .lumenflow.config.yaml
16
+ *
17
+ * @param projectDir - Project directory
18
+ * @returns Parsed configuration or null if not found
19
+ */
20
+ function readLumenFlowConfig(projectDir) {
21
+ const configPath = path.join(projectDir, '.lumenflow.config.yaml');
22
+ if (!fs.existsSync(configPath)) {
23
+ return null;
24
+ }
25
+ try {
26
+ const content = fs.readFileSync(configPath, 'utf-8');
27
+ return yaml.parse(content);
28
+ }
29
+ catch {
30
+ return null;
31
+ }
32
+ }
33
+ /**
34
+ * Get enforcement configuration from LumenFlow config
35
+ *
36
+ * @param config - LumenFlow configuration
37
+ * @returns Enforcement config or null if not enabled
38
+ */
39
+ function getEnforcementConfig(config) {
40
+ const enforcement = config?.agents?.clients?.['claude-code']?.enforcement;
41
+ if (!enforcement || !enforcement.hooks) {
42
+ return null;
43
+ }
44
+ return {
45
+ hooks: enforcement.hooks ?? false,
46
+ block_outside_worktree: enforcement.block_outside_worktree ?? false,
47
+ require_wu_for_edits: enforcement.require_wu_for_edits ?? false,
48
+ warn_on_stop_without_wu_done: enforcement.warn_on_stop_without_wu_done ?? false,
49
+ };
50
+ }
51
+ /**
52
+ * Read existing Claude settings.json
53
+ *
54
+ * @param projectDir - Project directory
55
+ * @returns Parsed settings or default structure
56
+ */
57
+ function readClaudeSettings(projectDir) {
58
+ const settingsPath = path.join(projectDir, '.claude', 'settings.json');
59
+ if (!fs.existsSync(settingsPath)) {
60
+ return {
61
+ $schema: 'https://json.schemastore.org/claude-code-settings.json',
62
+ permissions: {
63
+ allow: ['Bash', 'Read', 'Write', 'Edit'],
64
+ },
65
+ };
66
+ }
67
+ try {
68
+ const content = fs.readFileSync(settingsPath, 'utf-8');
69
+ return JSON.parse(content);
70
+ }
71
+ catch {
72
+ return {
73
+ $schema: 'https://json.schemastore.org/claude-code-settings.json',
74
+ };
75
+ }
76
+ }
77
+ /**
78
+ * Write Claude settings.json
79
+ *
80
+ * @param projectDir - Project directory
81
+ * @param settings - Settings to write
82
+ */
83
+ function writeClaudeSettings(projectDir, settings) {
84
+ const claudeDir = path.join(projectDir, '.claude');
85
+ const settingsPath = path.join(claudeDir, 'settings.json');
86
+ if (!fs.existsSync(claudeDir)) {
87
+ fs.mkdirSync(claudeDir, { recursive: true });
88
+ }
89
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
90
+ }
91
+ /**
92
+ * Write hook script to .claude/hooks/
93
+ *
94
+ * @param projectDir - Project directory
95
+ * @param filename - Script filename
96
+ * @param content - Script content
97
+ */
98
+ function writeHookScript(projectDir, filename, content) {
99
+ const hooksDir = path.join(projectDir, '.claude', 'hooks');
100
+ if (!fs.existsSync(hooksDir)) {
101
+ fs.mkdirSync(hooksDir, { recursive: true });
102
+ }
103
+ const scriptPath = path.join(hooksDir, filename);
104
+ fs.writeFileSync(scriptPath, content, { mode: 0o755 });
105
+ }
106
+ /**
107
+ * Merge generated hooks with existing hooks in settings
108
+ *
109
+ * @param existing - Existing settings
110
+ * @param generated - Generated hooks
111
+ * @returns Merged settings
112
+ */
113
+ // Complexity is acceptable for hook merging logic - alternative would over-abstract
114
+ // eslint-disable-next-line sonarjs/cognitive-complexity
115
+ function mergeHooksIntoSettings(existing, generated) {
116
+ const result = { ...existing };
117
+ if (!result.hooks) {
118
+ result.hooks = {};
119
+ }
120
+ // Merge PreToolUse hooks
121
+ if (generated.preToolUse) {
122
+ if (!result.hooks.PreToolUse) {
123
+ result.hooks.PreToolUse = [];
124
+ }
125
+ for (const newHook of generated.preToolUse) {
126
+ // Find existing entry with same matcher
127
+ const existingIndex = result.hooks.PreToolUse.findIndex((h) => h.matcher === newHook.matcher);
128
+ if (existingIndex >= 0) {
129
+ // Merge hooks into existing entry, avoiding duplicates
130
+ const existing = result.hooks.PreToolUse[existingIndex];
131
+ for (const hook of newHook.hooks) {
132
+ const isDuplicate = existing.hooks.some((h) => h.command === hook.command);
133
+ if (!isDuplicate) {
134
+ existing.hooks.push(hook);
135
+ }
136
+ }
137
+ }
138
+ else {
139
+ result.hooks.PreToolUse.push(newHook);
140
+ }
141
+ }
142
+ }
143
+ // Merge Stop hooks
144
+ if (generated.stop) {
145
+ if (!result.hooks.Stop) {
146
+ result.hooks.Stop = [];
147
+ }
148
+ for (const newHook of generated.stop) {
149
+ const existingIndex = result.hooks.Stop.findIndex((h) => h.matcher === newHook.matcher);
150
+ if (existingIndex >= 0) {
151
+ const existing = result.hooks.Stop[existingIndex];
152
+ for (const hook of newHook.hooks) {
153
+ const isDuplicate = existing.hooks.some((h) => h.command === hook.command);
154
+ if (!isDuplicate) {
155
+ existing.hooks.push(hook);
156
+ }
157
+ }
158
+ }
159
+ else {
160
+ result.hooks.Stop.push(newHook);
161
+ }
162
+ }
163
+ }
164
+ return result;
165
+ }
166
+ /**
167
+ * Sync enforcement hooks based on LumenFlow configuration.
168
+ *
169
+ * This function:
170
+ * 1. Reads .lumenflow.config.yaml
171
+ * 2. Checks if enforcement.hooks=true for claude-code
172
+ * 3. Generates and writes hook scripts
173
+ * 4. Updates .claude/settings.json with hook configuration
174
+ *
175
+ * @param projectDir - Project directory
176
+ * @returns True if hooks were synced, false if skipped
177
+ */
178
+ export async function syncEnforcementHooks(projectDir) {
179
+ // Read LumenFlow config
180
+ const config = readLumenFlowConfig(projectDir);
181
+ const enforcement = getEnforcementConfig(config);
182
+ // Skip if enforcement not enabled
183
+ if (!enforcement || !enforcement.hooks) {
184
+ return false;
185
+ }
186
+ // Generate hooks based on config
187
+ const generatedHooks = generateEnforcementHooks({
188
+ block_outside_worktree: enforcement.block_outside_worktree,
189
+ require_wu_for_edits: enforcement.require_wu_for_edits,
190
+ warn_on_stop_without_wu_done: enforcement.warn_on_stop_without_wu_done,
191
+ });
192
+ // Write hook scripts
193
+ if (enforcement.block_outside_worktree) {
194
+ writeHookScript(projectDir, 'enforce-worktree.sh', generateEnforceWorktreeScript());
195
+ }
196
+ if (enforcement.require_wu_for_edits) {
197
+ writeHookScript(projectDir, 'require-wu.sh', generateRequireWuScript());
198
+ }
199
+ if (enforcement.warn_on_stop_without_wu_done) {
200
+ writeHookScript(projectDir, 'warn-incomplete.sh', generateWarnIncompleteScript());
201
+ }
202
+ // Update settings.json
203
+ const existingSettings = readClaudeSettings(projectDir);
204
+ const updatedSettings = mergeHooksIntoSettings(existingSettings, generatedHooks);
205
+ writeClaudeSettings(projectDir, updatedSettings);
206
+ return true;
207
+ }
208
+ /**
209
+ * Remove enforcement hooks from settings.json
210
+ *
211
+ * @param projectDir - Project directory
212
+ */
213
+ export async function removeEnforcementHooks(projectDir) {
214
+ const settings = readClaudeSettings(projectDir);
215
+ if (!settings.hooks) {
216
+ return;
217
+ }
218
+ // Remove enforcement-related hooks
219
+ const enforcementCommands = ['enforce-worktree.sh', 'require-wu.sh', 'warn-incomplete.sh'];
220
+ if (settings.hooks.PreToolUse) {
221
+ settings.hooks.PreToolUse = settings.hooks.PreToolUse.map((entry) => ({
222
+ ...entry,
223
+ hooks: entry.hooks.filter((h) => !enforcementCommands.some((cmd) => h.command.includes(cmd))),
224
+ })).filter((entry) => entry.hooks.length > 0);
225
+ if (settings.hooks.PreToolUse.length === 0) {
226
+ delete settings.hooks.PreToolUse;
227
+ }
228
+ }
229
+ if (settings.hooks.Stop) {
230
+ settings.hooks.Stop = settings.hooks.Stop.map((entry) => ({
231
+ ...entry,
232
+ hooks: entry.hooks.filter((h) => !enforcementCommands.some((cmd) => h.command.includes(cmd))),
233
+ })).filter((entry) => entry.hooks.length > 0);
234
+ if (settings.hooks.Stop.length === 0) {
235
+ delete settings.hooks.Stop;
236
+ }
237
+ }
238
+ // Clean up empty hooks object
239
+ if (Object.keys(settings.hooks).length === 0) {
240
+ delete settings.hooks;
241
+ }
242
+ writeClaudeSettings(projectDir, settings);
243
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @file hooks/index.ts
3
+ * Claude Code enforcement hooks module (WU-1367)
4
+ */
5
+ export * from './enforcement-generator.js';
6
+ export * from './enforcement-checks.js';
7
+ export * from './enforcement-sync.js';