@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,209 @@
1
+ /**
2
+ * @file enforcement-checks.ts
3
+ * Runtime enforcement checks for LumenFlow workflow compliance (WU-1367)
4
+ *
5
+ * These functions can be used by hooks to validate operations.
6
+ * All checks implement graceful degradation: if state cannot be
7
+ * determined, operations are allowed.
8
+ */
9
+ // Note: fs operations use runtime-provided paths from LumenFlow configuration
10
+ import * as fs from 'node:fs';
11
+ import * as path from 'node:path';
12
+ /**
13
+ * Check if a Write/Edit operation should be allowed based on worktree enforcement.
14
+ *
15
+ * Implements graceful degradation: if LumenFlow state cannot be determined,
16
+ * the operation is allowed to prevent blocking legitimate work.
17
+ *
18
+ * @param input - Tool input with file_path and tool_name
19
+ * @param projectDir - Project directory (defaults to CLAUDE_PROJECT_DIR)
20
+ * @returns Check result with allowed status and reason
21
+ */
22
+ export async function checkWorktreeEnforcement(input, projectDir) {
23
+ const mainRepoPath = projectDir ?? process.env.CLAUDE_PROJECT_DIR;
24
+ // Graceful degradation: no project dir
25
+ if (!mainRepoPath) {
26
+ return {
27
+ allowed: true,
28
+ reason: 'graceful: CLAUDE_PROJECT_DIR not set',
29
+ };
30
+ }
31
+ const lumenflowDir = path.join(mainRepoPath, '.lumenflow');
32
+ const worktreesDir = path.join(mainRepoPath, 'worktrees');
33
+ // Graceful degradation: LumenFlow not configured
34
+ if (!fs.existsSync(lumenflowDir)) {
35
+ return {
36
+ allowed: true,
37
+ reason: 'graceful: LumenFlow not configured',
38
+ };
39
+ }
40
+ // No worktrees = no enforcement needed
41
+ if (!fs.existsSync(worktreesDir)) {
42
+ return {
43
+ allowed: true,
44
+ reason: 'no worktrees exist',
45
+ };
46
+ }
47
+ // Check for active worktrees
48
+ let worktreeCount = 0;
49
+ try {
50
+ const entries = fs.readdirSync(worktreesDir);
51
+ worktreeCount = entries.filter((e) => {
52
+ const stat = fs.statSync(path.join(worktreesDir, e));
53
+ return stat.isDirectory();
54
+ }).length;
55
+ }
56
+ catch {
57
+ return {
58
+ allowed: true,
59
+ reason: 'graceful: cannot read worktrees directory',
60
+ };
61
+ }
62
+ if (worktreeCount === 0) {
63
+ return {
64
+ allowed: true,
65
+ reason: 'no active worktrees',
66
+ };
67
+ }
68
+ // Resolve the file path
69
+ let resolvedPath;
70
+ try {
71
+ resolvedPath = path.resolve(input.file_path);
72
+ }
73
+ catch {
74
+ return {
75
+ allowed: true,
76
+ reason: 'graceful: cannot resolve file path',
77
+ };
78
+ }
79
+ // Allow if path is inside a worktree
80
+ if (resolvedPath.startsWith(worktreesDir + path.sep)) {
81
+ return {
82
+ allowed: true,
83
+ reason: 'path is inside worktree',
84
+ };
85
+ }
86
+ // Block if path is in main repo while worktrees exist
87
+ if (resolvedPath.startsWith(mainRepoPath + path.sep) || resolvedPath === mainRepoPath) {
88
+ const activeWorktrees = fs
89
+ .readdirSync(worktreesDir)
90
+ .filter((e) => fs.statSync(path.join(worktreesDir, e)).isDirectory())
91
+ .slice(0, 5)
92
+ .join(', ');
93
+ return {
94
+ allowed: false,
95
+ reason: `cannot write to main repo while worktrees exist (${activeWorktrees})`,
96
+ suggestion: 'cd to your worktree: cd worktrees/<lane>-wu-<id>/',
97
+ };
98
+ }
99
+ // Path is outside repo entirely - allow
100
+ return {
101
+ allowed: true,
102
+ reason: 'path is outside repository',
103
+ };
104
+ }
105
+ /**
106
+ * Check if a Write/Edit operation should be allowed based on WU requirement.
107
+ *
108
+ * @param input - Tool input with file_path and tool_name
109
+ * @param projectDir - Project directory
110
+ * @returns Check result with allowed status and reason
111
+ */
112
+ export async function checkWuRequirement(input, projectDir) {
113
+ const mainRepoPath = projectDir ?? process.env.CLAUDE_PROJECT_DIR;
114
+ if (!mainRepoPath) {
115
+ return {
116
+ allowed: true,
117
+ reason: 'graceful: CLAUDE_PROJECT_DIR not set',
118
+ };
119
+ }
120
+ const lumenflowDir = path.join(mainRepoPath, '.lumenflow');
121
+ const worktreesDir = path.join(mainRepoPath, 'worktrees');
122
+ const stateFile = path.join(lumenflowDir, 'state', 'wu-events.jsonl');
123
+ // Graceful degradation: LumenFlow not configured
124
+ if (!fs.existsSync(lumenflowDir)) {
125
+ return {
126
+ allowed: true,
127
+ reason: 'graceful: LumenFlow not configured',
128
+ };
129
+ }
130
+ // Check for active worktrees (indicates claimed WU)
131
+ if (fs.existsSync(worktreesDir)) {
132
+ try {
133
+ const entries = fs.readdirSync(worktreesDir);
134
+ const worktreeCount = entries.filter((e) => {
135
+ const stat = fs.statSync(path.join(worktreesDir, e));
136
+ return stat.isDirectory();
137
+ }).length;
138
+ if (worktreeCount > 0) {
139
+ return {
140
+ allowed: true,
141
+ reason: 'has active worktree (claimed WU)',
142
+ };
143
+ }
144
+ }
145
+ catch {
146
+ // Continue to state file check
147
+ }
148
+ }
149
+ // Check state file for in_progress WUs
150
+ if (fs.existsSync(stateFile)) {
151
+ try {
152
+ const content = fs.readFileSync(stateFile, 'utf-8');
153
+ if (content.includes('"status":"in_progress"')) {
154
+ return {
155
+ allowed: true,
156
+ reason: 'has in_progress WU in state',
157
+ };
158
+ }
159
+ }
160
+ catch {
161
+ return {
162
+ allowed: true,
163
+ reason: 'graceful: cannot read state file',
164
+ };
165
+ }
166
+ }
167
+ // No claimed WU found
168
+ return {
169
+ allowed: false,
170
+ reason: 'no WU claimed',
171
+ suggestion: 'pnpm wu:claim --id WU-XXXX --lane <Lane>',
172
+ };
173
+ }
174
+ /**
175
+ * Check if the current working directory is inside a worktree.
176
+ *
177
+ * @param cwd - Current working directory
178
+ * @param projectDir - Project directory
179
+ * @returns True if cwd is inside a worktree
180
+ */
181
+ export function isInsideWorktree(cwd, projectDir) {
182
+ const worktreesDir = path.join(projectDir, 'worktrees');
183
+ if (!fs.existsSync(worktreesDir)) {
184
+ return false;
185
+ }
186
+ const resolvedCwd = path.resolve(cwd);
187
+ return resolvedCwd.startsWith(worktreesDir + path.sep);
188
+ }
189
+ /**
190
+ * Get list of active worktrees.
191
+ *
192
+ * @param projectDir - Project directory
193
+ * @returns Array of worktree names
194
+ */
195
+ export function getActiveWorktrees(projectDir) {
196
+ const worktreesDir = path.join(projectDir, 'worktrees');
197
+ if (!fs.existsSync(worktreesDir)) {
198
+ return [];
199
+ }
200
+ try {
201
+ return fs.readdirSync(worktreesDir).filter((e) => {
202
+ const stat = fs.statSync(path.join(worktreesDir, e));
203
+ return stat.isDirectory();
204
+ });
205
+ }
206
+ catch {
207
+ return [];
208
+ }
209
+ }
@@ -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
+ }