@lumenflow/cli 2.7.0 → 2.8.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.
- package/README.md +120 -105
- package/dist/__tests__/agent-spawn-coordination.test.js +451 -0
- package/dist/__tests__/commands/integrate.test.js +165 -0
- package/dist/__tests__/gates-config.test.js +0 -1
- package/dist/__tests__/hooks/enforcement.test.js +279 -0
- package/dist/__tests__/init-greenfield.test.js +247 -0
- package/dist/__tests__/init-quick-ref.test.js +0 -1
- package/dist/__tests__/init-template-portability.test.js +0 -1
- package/dist/__tests__/init.test.js +27 -0
- package/dist/__tests__/initiative-e2e.test.js +442 -0
- package/dist/__tests__/initiative-plan-replacement.test.js +0 -1
- package/dist/__tests__/memory-integration.test.js +333 -0
- package/dist/__tests__/release.test.js +1 -1
- package/dist/__tests__/safe-git.test.js +0 -1
- package/dist/__tests__/state-doctor.test.js +54 -0
- package/dist/__tests__/sync-templates.test.js +255 -0
- package/dist/__tests__/wu-create-required-fields.test.js +121 -0
- package/dist/__tests__/wu-done-auto-cleanup.test.js +135 -0
- package/dist/__tests__/wu-lifecycle-integration.test.js +388 -0
- package/dist/backlog-prune.js +0 -1
- package/dist/cli-entry-point.js +0 -1
- package/dist/commands/integrate.js +229 -0
- package/dist/docs-sync.js +46 -0
- package/dist/doctor.js +0 -2
- package/dist/gates.js +0 -7
- package/dist/hooks/enforcement-checks.js +209 -0
- package/dist/hooks/enforcement-generator.js +365 -0
- package/dist/hooks/enforcement-sync.js +243 -0
- package/dist/hooks/index.js +7 -0
- package/dist/init.js +256 -13
- package/dist/initiative-add-wu.js +0 -2
- package/dist/initiative-create.js +0 -3
- package/dist/initiative-edit.js +0 -5
- package/dist/initiative-plan.js +0 -1
- package/dist/initiative-remove-wu.js +0 -2
- package/dist/lane-health.js +0 -2
- package/dist/lane-suggest.js +0 -1
- package/dist/mem-checkpoint.js +0 -2
- package/dist/mem-cleanup.js +0 -2
- package/dist/mem-context.js +0 -3
- package/dist/mem-create.js +0 -2
- package/dist/mem-delete.js +0 -3
- package/dist/mem-inbox.js +0 -2
- package/dist/mem-index.js +0 -1
- package/dist/mem-init.js +0 -2
- package/dist/mem-profile.js +0 -1
- package/dist/mem-promote.js +0 -1
- package/dist/mem-ready.js +0 -2
- package/dist/mem-signal.js +0 -2
- package/dist/mem-start.js +0 -2
- package/dist/mem-summarize.js +0 -2
- package/dist/metrics-cli.js +1 -1
- package/dist/metrics-snapshot.js +1 -1
- package/dist/onboarding-smoke-test.js +0 -5
- package/dist/orchestrate-init-status.js +0 -1
- package/dist/orchestrate-initiative.js +0 -1
- package/dist/orchestrate-monitor.js +0 -1
- package/dist/plan-create.js +0 -2
- package/dist/plan-edit.js +0 -2
- package/dist/plan-link.js +0 -2
- package/dist/plan-promote.js +0 -2
- package/dist/signal-cleanup.js +0 -4
- package/dist/state-bootstrap.js +0 -1
- package/dist/state-cleanup.js +0 -4
- package/dist/state-doctor-fix.js +5 -8
- package/dist/state-doctor.js +0 -11
- package/dist/sync-templates.js +188 -34
- package/dist/wu-block.js +100 -48
- package/dist/wu-claim.js +1 -22
- package/dist/wu-cleanup.js +0 -1
- package/dist/wu-create.js +0 -2
- package/dist/wu-done-auto-cleanup.js +139 -0
- package/dist/wu-done.js +11 -4
- package/dist/wu-edit.js +0 -12
- package/dist/wu-preflight.js +0 -1
- package/dist/wu-prep.js +0 -1
- package/dist/wu-proto.js +0 -1
- package/dist/wu-spawn.js +0 -3
- package/dist/wu-unblock.js +0 -2
- package/dist/wu-validate.js +0 -1
- package/package.json +8 -7
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file enforcement-generator.ts
|
|
3
|
+
* Generates Claude Code enforcement hooks based on configuration (WU-1367)
|
|
4
|
+
*
|
|
5
|
+
* This module generates hook configurations that can be written to
|
|
6
|
+
* .claude/settings.json to enforce LumenFlow workflow compliance.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Generate enforcement hooks based on configuration.
|
|
10
|
+
*
|
|
11
|
+
* @param config - Enforcement configuration
|
|
12
|
+
* @returns Generated hooks structure for settings.json
|
|
13
|
+
*/
|
|
14
|
+
export function generateEnforcementHooks(config) {
|
|
15
|
+
const hooks = {};
|
|
16
|
+
const preToolUseHooks = [];
|
|
17
|
+
// Generate PreToolUse hooks for Write/Edit operations
|
|
18
|
+
if (config.block_outside_worktree || config.require_wu_for_edits) {
|
|
19
|
+
const writeEditHooks = [];
|
|
20
|
+
if (config.block_outside_worktree) {
|
|
21
|
+
writeEditHooks.push({
|
|
22
|
+
type: 'command',
|
|
23
|
+
command: '$CLAUDE_PROJECT_DIR/.claude/hooks/enforce-worktree.sh',
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
if (config.require_wu_for_edits) {
|
|
27
|
+
writeEditHooks.push({
|
|
28
|
+
type: 'command',
|
|
29
|
+
command: '$CLAUDE_PROJECT_DIR/.claude/hooks/require-wu.sh',
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
if (writeEditHooks.length > 0) {
|
|
33
|
+
preToolUseHooks.push({
|
|
34
|
+
matcher: 'Write|Edit',
|
|
35
|
+
hooks: writeEditHooks,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (preToolUseHooks.length > 0) {
|
|
40
|
+
hooks.preToolUse = preToolUseHooks;
|
|
41
|
+
}
|
|
42
|
+
// Generate Stop hook for session completion warning
|
|
43
|
+
if (config.warn_on_stop_without_wu_done) {
|
|
44
|
+
hooks.stop = [
|
|
45
|
+
{
|
|
46
|
+
matcher: '.*',
|
|
47
|
+
hooks: [
|
|
48
|
+
{
|
|
49
|
+
type: 'command',
|
|
50
|
+
command: '$CLAUDE_PROJECT_DIR/.claude/hooks/warn-incomplete.sh',
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
];
|
|
55
|
+
}
|
|
56
|
+
return hooks;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Generate the enforce-worktree.sh hook script content.
|
|
60
|
+
*
|
|
61
|
+
* This hook blocks Write/Edit operations when not in a worktree.
|
|
62
|
+
* Implements graceful degradation: allows operations if LumenFlow
|
|
63
|
+
* state cannot be determined.
|
|
64
|
+
*/
|
|
65
|
+
export function generateEnforceWorktreeScript() {
|
|
66
|
+
// Note: Shell variable escapes (\$, \") are intentional for the generated bash script
|
|
67
|
+
/* eslint-disable no-useless-escape */
|
|
68
|
+
return `#!/bin/bash
|
|
69
|
+
#
|
|
70
|
+
# enforce-worktree.sh (WU-1367)
|
|
71
|
+
#
|
|
72
|
+
# PreToolUse hook that blocks Write/Edit when not in a worktree.
|
|
73
|
+
# Graceful degradation: allows operations if state cannot be determined.
|
|
74
|
+
#
|
|
75
|
+
# Exit codes:
|
|
76
|
+
# 0 = Allow operation
|
|
77
|
+
# 2 = Block operation (stderr shown to Claude as guidance)
|
|
78
|
+
#
|
|
79
|
+
|
|
80
|
+
set -euo pipefail
|
|
81
|
+
|
|
82
|
+
SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
|
|
83
|
+
|
|
84
|
+
# Graceful degradation: if we can't determine state, allow the operation
|
|
85
|
+
graceful_allow() {
|
|
86
|
+
local reason="\$1"
|
|
87
|
+
# Optionally log for debugging
|
|
88
|
+
# echo "[enforce-worktree] Graceful allow: \$reason" >&2
|
|
89
|
+
exit 0
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
# Derive repo paths from CLAUDE_PROJECT_DIR
|
|
93
|
+
if [[ -z "\${CLAUDE_PROJECT_DIR:-}" ]]; then
|
|
94
|
+
graceful_allow "CLAUDE_PROJECT_DIR not set"
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
MAIN_REPO_PATH="\$CLAUDE_PROJECT_DIR"
|
|
98
|
+
WORKTREES_DIR="\${MAIN_REPO_PATH}/worktrees"
|
|
99
|
+
LUMENFLOW_DIR="\${MAIN_REPO_PATH}/.lumenflow"
|
|
100
|
+
|
|
101
|
+
# Check if .lumenflow exists (LumenFlow is configured)
|
|
102
|
+
if [[ ! -d "\$LUMENFLOW_DIR" ]]; then
|
|
103
|
+
graceful_allow "No .lumenflow directory (LumenFlow not configured)"
|
|
104
|
+
fi
|
|
105
|
+
|
|
106
|
+
# Read JSON input from stdin
|
|
107
|
+
INPUT=\$(cat)
|
|
108
|
+
|
|
109
|
+
if [[ -z "\$INPUT" ]]; then
|
|
110
|
+
graceful_allow "No input provided"
|
|
111
|
+
fi
|
|
112
|
+
|
|
113
|
+
# Parse JSON with Python
|
|
114
|
+
TMPFILE=\$(mktemp)
|
|
115
|
+
echo "\$INPUT" > "\$TMPFILE"
|
|
116
|
+
|
|
117
|
+
PARSE_RESULT=\$(python3 -c "
|
|
118
|
+
import json
|
|
119
|
+
import sys
|
|
120
|
+
try:
|
|
121
|
+
with open('\$TMPFILE', 'r') as f:
|
|
122
|
+
data = json.load(f)
|
|
123
|
+
tool_name = data.get('tool_name', '')
|
|
124
|
+
tool_input = data.get('tool_input', {})
|
|
125
|
+
if not isinstance(tool_input, dict):
|
|
126
|
+
tool_input = {}
|
|
127
|
+
file_path = tool_input.get('file_path', '')
|
|
128
|
+
print('OK')
|
|
129
|
+
print(tool_name if tool_name else '')
|
|
130
|
+
print(file_path if file_path else '')
|
|
131
|
+
except Exception as e:
|
|
132
|
+
print('ERROR')
|
|
133
|
+
print(str(e))
|
|
134
|
+
print('')
|
|
135
|
+
" 2>&1)
|
|
136
|
+
|
|
137
|
+
rm -f "\$TMPFILE"
|
|
138
|
+
|
|
139
|
+
# Parse the result
|
|
140
|
+
PARSE_STATUS=\$(echo "\$PARSE_RESULT" | head -1)
|
|
141
|
+
TOOL_NAME=\$(echo "\$PARSE_RESULT" | sed -n '2p')
|
|
142
|
+
FILE_PATH=\$(echo "\$PARSE_RESULT" | sed -n '3p')
|
|
143
|
+
|
|
144
|
+
if [[ "\$PARSE_STATUS" != "OK" ]]; then
|
|
145
|
+
graceful_allow "JSON parse failed"
|
|
146
|
+
fi
|
|
147
|
+
|
|
148
|
+
# Only process Write and Edit tools
|
|
149
|
+
if [[ "\$TOOL_NAME" != "Write" && "\$TOOL_NAME" != "Edit" ]]; then
|
|
150
|
+
exit 0
|
|
151
|
+
fi
|
|
152
|
+
|
|
153
|
+
if [[ -z "\$FILE_PATH" ]]; then
|
|
154
|
+
graceful_allow "No file_path in input"
|
|
155
|
+
fi
|
|
156
|
+
|
|
157
|
+
# Check if any worktrees exist
|
|
158
|
+
if [[ ! -d "\$WORKTREES_DIR" ]]; then
|
|
159
|
+
exit 0 # No worktrees = no enforcement needed
|
|
160
|
+
fi
|
|
161
|
+
|
|
162
|
+
WORKTREE_COUNT=\$(find "\$WORKTREES_DIR" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | wc -l)
|
|
163
|
+
if [[ "\$WORKTREE_COUNT" -eq 0 ]]; then
|
|
164
|
+
exit 0 # No active worktrees
|
|
165
|
+
fi
|
|
166
|
+
|
|
167
|
+
# Resolve the file path
|
|
168
|
+
RESOLVED_PATH=\$(realpath -m "\$FILE_PATH" 2>/dev/null || echo "\$FILE_PATH")
|
|
169
|
+
|
|
170
|
+
# Allow if path is inside a worktree
|
|
171
|
+
if [[ "\$RESOLVED_PATH" == "\${WORKTREES_DIR}/"* ]]; then
|
|
172
|
+
exit 0
|
|
173
|
+
fi
|
|
174
|
+
|
|
175
|
+
# Block if path is in main repo while worktrees exist
|
|
176
|
+
if [[ "\$RESOLVED_PATH" == "\${MAIN_REPO_PATH}/"* || "\$RESOLVED_PATH" == "\${MAIN_REPO_PATH}" ]]; then
|
|
177
|
+
ACTIVE_WORKTREES=\$(find "\$WORKTREES_DIR" -mindepth 1 -maxdepth 1 -type d -printf '%f\\n' 2>/dev/null | head -5 | tr '\\n' ', ' | sed 's/,\$//')
|
|
178
|
+
|
|
179
|
+
echo "" >&2
|
|
180
|
+
echo "=== Worktree Enforcement ===" >&2
|
|
181
|
+
echo "" >&2
|
|
182
|
+
echo "BLOCKED: \$TOOL_NAME to main repo" >&2
|
|
183
|
+
echo "" >&2
|
|
184
|
+
echo "Active worktrees: \${ACTIVE_WORKTREES:-none detected}" >&2
|
|
185
|
+
echo "" >&2
|
|
186
|
+
echo "USE INSTEAD:" >&2
|
|
187
|
+
echo " 1. cd to your worktree: cd worktrees/<lane>-wu-<id>/" >&2
|
|
188
|
+
echo " 2. Make your edits in the worktree" >&2
|
|
189
|
+
echo "" >&2
|
|
190
|
+
echo "See: LUMENFLOW.md for worktree discipline" >&2
|
|
191
|
+
echo "==============================" >&2
|
|
192
|
+
exit 2
|
|
193
|
+
fi
|
|
194
|
+
|
|
195
|
+
# Path is outside repo entirely - allow
|
|
196
|
+
exit 0
|
|
197
|
+
`;
|
|
198
|
+
/* eslint-enable no-useless-escape */
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Generate the require-wu.sh hook script content.
|
|
202
|
+
*
|
|
203
|
+
* This hook blocks Write/Edit operations when no WU is claimed.
|
|
204
|
+
* Implements graceful degradation: allows operations if LumenFlow
|
|
205
|
+
* state cannot be determined.
|
|
206
|
+
*/
|
|
207
|
+
export function generateRequireWuScript() {
|
|
208
|
+
// Note: Shell variable escapes (\$, \") are intentional for the generated bash script
|
|
209
|
+
/* eslint-disable no-useless-escape */
|
|
210
|
+
return `#!/bin/bash
|
|
211
|
+
#
|
|
212
|
+
# require-wu.sh (WU-1367)
|
|
213
|
+
#
|
|
214
|
+
# PreToolUse hook that blocks Write/Edit when no WU is claimed.
|
|
215
|
+
# Graceful degradation: allows operations if state cannot be determined.
|
|
216
|
+
#
|
|
217
|
+
# Exit codes:
|
|
218
|
+
# 0 = Allow operation
|
|
219
|
+
# 2 = Block operation (stderr shown to Claude as guidance)
|
|
220
|
+
#
|
|
221
|
+
|
|
222
|
+
set -euo pipefail
|
|
223
|
+
|
|
224
|
+
SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
|
|
225
|
+
|
|
226
|
+
# Graceful degradation
|
|
227
|
+
graceful_allow() {
|
|
228
|
+
local reason="\$1"
|
|
229
|
+
exit 0
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if [[ -z "\${CLAUDE_PROJECT_DIR:-}" ]]; then
|
|
233
|
+
graceful_allow "CLAUDE_PROJECT_DIR not set"
|
|
234
|
+
fi
|
|
235
|
+
|
|
236
|
+
MAIN_REPO_PATH="\$CLAUDE_PROJECT_DIR"
|
|
237
|
+
WORKTREES_DIR="\${MAIN_REPO_PATH}/worktrees"
|
|
238
|
+
LUMENFLOW_DIR="\${MAIN_REPO_PATH}/.lumenflow"
|
|
239
|
+
STATE_FILE="\${LUMENFLOW_DIR}/state/wu-events.jsonl"
|
|
240
|
+
|
|
241
|
+
# Check if LumenFlow is configured
|
|
242
|
+
if [[ ! -d "\$LUMENFLOW_DIR" ]]; then
|
|
243
|
+
graceful_allow "No .lumenflow directory"
|
|
244
|
+
fi
|
|
245
|
+
|
|
246
|
+
# Read JSON input
|
|
247
|
+
INPUT=\$(cat)
|
|
248
|
+
if [[ -z "\$INPUT" ]]; then
|
|
249
|
+
graceful_allow "No input"
|
|
250
|
+
fi
|
|
251
|
+
|
|
252
|
+
# Parse JSON
|
|
253
|
+
TMPFILE=\$(mktemp)
|
|
254
|
+
echo "\$INPUT" > "\$TMPFILE"
|
|
255
|
+
|
|
256
|
+
TOOL_NAME=\$(python3 -c "
|
|
257
|
+
import json
|
|
258
|
+
try:
|
|
259
|
+
with open('\$TMPFILE', 'r') as f:
|
|
260
|
+
data = json.load(f)
|
|
261
|
+
print(data.get('tool_name', ''))
|
|
262
|
+
except:
|
|
263
|
+
print('')
|
|
264
|
+
" 2>/dev/null || echo "")
|
|
265
|
+
|
|
266
|
+
rm -f "\$TMPFILE"
|
|
267
|
+
|
|
268
|
+
# Only check Write and Edit
|
|
269
|
+
if [[ "\$TOOL_NAME" != "Write" && "\$TOOL_NAME" != "Edit" ]]; then
|
|
270
|
+
exit 0
|
|
271
|
+
fi
|
|
272
|
+
|
|
273
|
+
# Check for active worktrees (indicates claimed WU)
|
|
274
|
+
if [[ -d "\$WORKTREES_DIR" ]]; then
|
|
275
|
+
WORKTREE_COUNT=\$(find "\$WORKTREES_DIR" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | wc -l)
|
|
276
|
+
if [[ "\$WORKTREE_COUNT" -gt 0 ]]; then
|
|
277
|
+
exit 0 # Has worktrees = has claimed WU
|
|
278
|
+
fi
|
|
279
|
+
fi
|
|
280
|
+
|
|
281
|
+
# Check state file for in_progress WUs
|
|
282
|
+
if [[ -f "\$STATE_FILE" ]]; then
|
|
283
|
+
# Look for any WU with in_progress status
|
|
284
|
+
if grep -q '"status":"in_progress"' "\$STATE_FILE" 2>/dev/null; then
|
|
285
|
+
exit 0 # Has in_progress WU
|
|
286
|
+
fi
|
|
287
|
+
fi
|
|
288
|
+
|
|
289
|
+
# No claimed WU found
|
|
290
|
+
echo "" >&2
|
|
291
|
+
echo "=== WU Enforcement ===" >&2
|
|
292
|
+
echo "" >&2
|
|
293
|
+
echo "BLOCKED: \$TOOL_NAME without claimed WU" >&2
|
|
294
|
+
echo "" >&2
|
|
295
|
+
echo "You must claim a WU before making edits:" >&2
|
|
296
|
+
echo " pnpm wu:claim --id WU-XXXX --lane <Lane>" >&2
|
|
297
|
+
echo " cd worktrees/<lane>-wu-xxxx" >&2
|
|
298
|
+
echo "" >&2
|
|
299
|
+
echo "Or create a new WU:" >&2
|
|
300
|
+
echo " pnpm wu:create --lane <Lane> --title \"Description\"" >&2
|
|
301
|
+
echo "" >&2
|
|
302
|
+
echo "See: LUMENFLOW.md for workflow details" >&2
|
|
303
|
+
echo "======================" >&2
|
|
304
|
+
exit 2
|
|
305
|
+
`;
|
|
306
|
+
/* eslint-enable no-useless-escape */
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Generate the warn-incomplete.sh hook script content.
|
|
310
|
+
*
|
|
311
|
+
* This Stop hook warns when session ends without wu:done.
|
|
312
|
+
* Always exits 0 (warning only, never blocks).
|
|
313
|
+
*/
|
|
314
|
+
export function generateWarnIncompleteScript() {
|
|
315
|
+
// Note: Shell variable escapes (\$, \") are intentional for the generated bash script
|
|
316
|
+
/* eslint-disable no-useless-escape */
|
|
317
|
+
return `#!/bin/bash
|
|
318
|
+
#
|
|
319
|
+
# warn-incomplete.sh (WU-1367)
|
|
320
|
+
#
|
|
321
|
+
# Stop hook that warns when session ends without wu:done.
|
|
322
|
+
# This is advisory only - never blocks session termination.
|
|
323
|
+
#
|
|
324
|
+
# Exit codes:
|
|
325
|
+
# 0 = Always (warnings only)
|
|
326
|
+
#
|
|
327
|
+
|
|
328
|
+
SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
|
|
329
|
+
|
|
330
|
+
if [[ -z "\${CLAUDE_PROJECT_DIR:-}" ]]; then
|
|
331
|
+
exit 0
|
|
332
|
+
fi
|
|
333
|
+
|
|
334
|
+
MAIN_REPO_PATH="\$CLAUDE_PROJECT_DIR"
|
|
335
|
+
WORKTREES_DIR="\${MAIN_REPO_PATH}/worktrees"
|
|
336
|
+
|
|
337
|
+
# Check for active worktrees
|
|
338
|
+
if [[ ! -d "\$WORKTREES_DIR" ]]; then
|
|
339
|
+
exit 0
|
|
340
|
+
fi
|
|
341
|
+
|
|
342
|
+
WORKTREE_COUNT=\$(find "\$WORKTREES_DIR" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | wc -l)
|
|
343
|
+
if [[ "\$WORKTREE_COUNT" -eq 0 ]]; then
|
|
344
|
+
exit 0
|
|
345
|
+
fi
|
|
346
|
+
|
|
347
|
+
# Get active worktree names
|
|
348
|
+
ACTIVE_WORKTREES=\$(find "\$WORKTREES_DIR" -mindepth 1 -maxdepth 1 -type d -printf '%f\\n' 2>/dev/null | head -5 | tr '\\n' ', ' | sed 's/,\$//')
|
|
349
|
+
|
|
350
|
+
echo "" >&2
|
|
351
|
+
echo "=== Session Completion Reminder ===" >&2
|
|
352
|
+
echo "" >&2
|
|
353
|
+
echo "You have active worktrees: \$ACTIVE_WORKTREES" >&2
|
|
354
|
+
echo "" >&2
|
|
355
|
+
echo "If your work is complete, remember to run:" >&2
|
|
356
|
+
echo " pnpm wu:prep --id WU-XXXX (from worktree)" >&2
|
|
357
|
+
echo " pnpm wu:done --id WU-XXXX (from main)" >&2
|
|
358
|
+
echo "" >&2
|
|
359
|
+
echo "If work is incomplete, it will be preserved in the worktree." >&2
|
|
360
|
+
echo "====================================" >&2
|
|
361
|
+
|
|
362
|
+
exit 0
|
|
363
|
+
`;
|
|
364
|
+
/* eslint-enable no-useless-escape */
|
|
365
|
+
}
|
|
@@ -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
|
+
}
|