@lumenflow/cli 4.0.0 → 4.1.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 (125) hide show
  1. package/dist/gate-defaults.js +12 -2
  2. package/dist/gate-defaults.js.map +1 -1
  3. package/dist/gates-runners.js +271 -2
  4. package/dist/gates-runners.js.map +1 -1
  5. package/dist/gates.js +14 -1
  6. package/dist/gates.js.map +1 -1
  7. package/dist/wu-done-preflight.js +4 -3
  8. package/dist/wu-done-preflight.js.map +1 -1
  9. package/dist/wu-done.js +1 -1
  10. package/dist/wu-done.js.map +1 -1
  11. package/dist/wu-prep.js +24 -5
  12. package/dist/wu-prep.js.map +1 -1
  13. package/dist/wu-state-mutation-ownership.js +3 -1
  14. package/dist/wu-state-mutation-ownership.js.map +1 -1
  15. package/package.json +8 -8
  16. package/packs/agent-runtime/.turbo/turbo-build.log +1 -1
  17. package/packs/agent-runtime/package.json +1 -1
  18. package/packs/sidekick/.turbo/turbo-build.log +1 -1
  19. package/packs/sidekick/package.json +1 -1
  20. package/packs/software-delivery/.turbo/turbo-build.log +1 -1
  21. package/packs/software-delivery/package.json +1 -1
  22. package/dist/chunk-2D2VOCA4.js +0 -37
  23. package/dist/chunk-2D5KFYGX.js +0 -284
  24. package/dist/chunk-2GXVIN57.js +0 -14072
  25. package/dist/chunk-2MQ7HZWZ.js +0 -26
  26. package/dist/chunk-2UFQ3A3C.js +0 -643
  27. package/dist/chunk-3RG5ZIWI.js +0 -10
  28. package/dist/chunk-4N74J3UT.js +0 -15
  29. package/dist/chunk-5GTOXFYR.js +0 -392
  30. package/dist/chunk-5VY6MQMC.js +0 -240
  31. package/dist/chunk-67XVPMRY.js +0 -1297
  32. package/dist/chunk-6HO4GWJE.js +0 -164
  33. package/dist/chunk-6W5XHWYV.js +0 -1890
  34. package/dist/chunk-6X4EMYJQ.js +0 -64
  35. package/dist/chunk-6XYXI2NQ.js +0 -772
  36. package/dist/chunk-7ANSOV6Q.js +0 -285
  37. package/dist/chunk-A624LFLB.js +0 -1380
  38. package/dist/chunk-ADN5NHG4.js +0 -126
  39. package/dist/chunk-B7YJYJKG.js +0 -33
  40. package/dist/chunk-CCLHCPKG.js +0 -210
  41. package/dist/chunk-CK36VROC.js +0 -1584
  42. package/dist/chunk-D3UOFRSB.js +0 -81
  43. package/dist/chunk-DFR4DJBM.js +0 -230
  44. package/dist/chunk-DSYBDHYH.js +0 -79
  45. package/dist/chunk-DWMLTXKQ.js +0 -1176
  46. package/dist/chunk-E3REJTAJ.js +0 -28
  47. package/dist/chunk-EA3IVO64.js +0 -633
  48. package/dist/chunk-EK2AKZKD.js +0 -55
  49. package/dist/chunk-ELD7JTTT.js +0 -343
  50. package/dist/chunk-EX6TT2XI.js +0 -195
  51. package/dist/chunk-EXINSFZE.js +0 -82
  52. package/dist/chunk-EZ6ZBYBM.js +0 -510
  53. package/dist/chunk-FBKAPTJ2.js +0 -16
  54. package/dist/chunk-FVLV5RYH.js +0 -1118
  55. package/dist/chunk-GDNSBQVK.js +0 -2485
  56. package/dist/chunk-GPQHMBNN.js +0 -278
  57. package/dist/chunk-GTFJB67L.js +0 -68
  58. package/dist/chunk-HANJXVKW.js +0 -1127
  59. package/dist/chunk-HEVS5YLD.js +0 -269
  60. package/dist/chunk-HMEVZKPQ.js +0 -9
  61. package/dist/chunk-HRGSYNLM.js +0 -3511
  62. package/dist/chunk-ISZR5N4K.js +0 -60
  63. package/dist/chunk-J6SUPR2C.js +0 -226
  64. package/dist/chunk-JERYVEIZ.js +0 -244
  65. package/dist/chunk-JHHWGL2N.js +0 -87
  66. package/dist/chunk-JONWQUB5.js +0 -775
  67. package/dist/chunk-K2DIWWDM.js +0 -1766
  68. package/dist/chunk-KY4PGL5V.js +0 -969
  69. package/dist/chunk-L737LQ4C.js +0 -1285
  70. package/dist/chunk-LFTWYIB2.js +0 -497
  71. package/dist/chunk-LV47RFNJ.js +0 -41
  72. package/dist/chunk-MKSAITI7.js +0 -15
  73. package/dist/chunk-MZ7RKIX4.js +0 -212
  74. package/dist/chunk-NAP6CFSO.js +0 -84
  75. package/dist/chunk-ND6MY37M.js +0 -16
  76. package/dist/chunk-NMG736UR.js +0 -683
  77. package/dist/chunk-NRAXROED.js +0 -32
  78. package/dist/chunk-NRIZR3A7.js +0 -690
  79. package/dist/chunk-NX43BG3M.js +0 -233
  80. package/dist/chunk-O645XLSI.js +0 -297
  81. package/dist/chunk-OMJD6A3S.js +0 -235
  82. package/dist/chunk-QB6SJD4T.js +0 -430
  83. package/dist/chunk-QFSTL4J3.js +0 -276
  84. package/dist/chunk-QLGDFMFX.js +0 -212
  85. package/dist/chunk-RIAAGL2E.js +0 -13
  86. package/dist/chunk-RWO5XMZ6.js +0 -86
  87. package/dist/chunk-RXRKBBSM.js +0 -149
  88. package/dist/chunk-RZOZMML6.js +0 -363
  89. package/dist/chunk-U7I7FS7T.js +0 -113
  90. package/dist/chunk-UI42RODY.js +0 -717
  91. package/dist/chunk-UTVMVSCO.js +0 -519
  92. package/dist/chunk-V6OJGLBA.js +0 -1746
  93. package/dist/chunk-W2JHVH7D.js +0 -152
  94. package/dist/chunk-WD3Y7VQN.js +0 -280
  95. package/dist/chunk-WOCTQ5MS.js +0 -303
  96. package/dist/chunk-WZR3ZUNN.js +0 -696
  97. package/dist/chunk-XGI665H7.js +0 -150
  98. package/dist/chunk-XKY65P2T.js +0 -304
  99. package/dist/chunk-Y4CQZY65.js +0 -57
  100. package/dist/chunk-YFEXKLVE.js +0 -194
  101. package/dist/chunk-YHO3HS5X.js +0 -287
  102. package/dist/chunk-YLS7AZSX.js +0 -738
  103. package/dist/chunk-ZE473AO6.js +0 -49
  104. package/dist/chunk-ZF747T3O.js +0 -644
  105. package/dist/chunk-ZHCZHZH3.js +0 -43
  106. package/dist/chunk-ZZNZX2XY.js +0 -87
  107. package/dist/constants-7QAP3VQ4.js +0 -23
  108. package/dist/dist-IY3UUMWK.js +0 -33
  109. package/dist/invariants-runner-W5RGHCSU.js +0 -27
  110. package/dist/lane-lock-6J36HD5O.js +0 -35
  111. package/dist/mem-checkpoint-core-EANG2GVN.js +0 -14
  112. package/dist/mem-signal-core-2LZ2WYHW.js +0 -19
  113. package/dist/memory-store-OLB5FO7K.js +0 -18
  114. package/dist/service-6BYCOCO5.js +0 -13
  115. package/dist/spawn-policy-resolver-NTSZYQ6R.js +0 -17
  116. package/dist/spawn-task-builder-R4E2BHSW.js +0 -22
  117. package/dist/wu-done-pr-WLFFFEPJ.js +0 -25
  118. package/dist/wu-done-validation-3J5E36FE.js +0 -30
  119. package/dist/wu-duplicate-id-detector-5S7JHELK.js +0 -232
  120. package/packs/agent-runtime/.turbo/turbo-test.log +0 -21
  121. package/packs/agent-runtime/.turbo/turbo-typecheck.log +0 -4
  122. package/packs/sidekick/.turbo/turbo-test.log +0 -157
  123. package/packs/sidekick/.turbo/turbo-typecheck.log +0 -4
  124. package/packs/software-delivery/.turbo/turbo-test.log +0 -32
  125. package/packs/software-delivery/.turbo/turbo-typecheck.log +0 -4
@@ -1,28 +0,0 @@
1
- import {
2
- createGitForPath
3
- } from "./chunk-2UFQ3A3C.js";
4
- import {
5
- BRANCHES
6
- } from "./chunk-DWMLTXKQ.js";
7
-
8
- // ../core/dist/core/worktree-guard.js
9
- import path from "path";
10
- var WORKTREE_PATH_PATTERN = /worktrees\/([\w-]+)-wu-(\d+)/;
11
- async function isMainBranch(options = {}) {
12
- const git = options.git || createGitForPath(process.cwd());
13
- const branch = await git.getCurrentBranch();
14
- return branch === BRANCHES.MAIN || branch === BRANCHES.MASTER;
15
- }
16
- function normalizePath(p) {
17
- return p.replace(/\\/g, "/").split(path.sep).join("/");
18
- }
19
- function isInWorktree(options = {}) {
20
- const cwd = options.cwd || process.cwd();
21
- const normalizedPath = normalizePath(cwd);
22
- return WORKTREE_PATH_PATTERN.test(normalizedPath);
23
- }
24
-
25
- export {
26
- isMainBranch,
27
- isInWorktree
28
- };
@@ -1,633 +0,0 @@
1
- import {
2
- loadSignals,
3
- markSignalsAsRead
4
- } from "./chunk-OMJD6A3S.js";
5
- import {
6
- createWuPaths
7
- } from "./chunk-6HO4GWJE.js";
8
- import {
9
- CLAUDE_HOOKS,
10
- DIRECTORIES,
11
- getHookCommand
12
- } from "./chunk-DWMLTXKQ.js";
13
-
14
- // src/hooks/generators/enforce-worktree.ts
15
- var DEFAULT_WORKTREES_DIR_SEGMENT = DIRECTORIES.WORKTREES.replace(/\/+$/g, "");
16
- var DEFAULT_WU_ALLOWLIST_PREFIX = DIRECTORIES.WU_DIR;
17
- function normalizeDirectorySegment(value, fallback) {
18
- const normalized = value.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
19
- return normalized.length > 0 ? normalized : fallback;
20
- }
21
- function ensureTrailingSlash(value) {
22
- return value.endsWith("/") ? value : `${value}/`;
23
- }
24
- function generateEnforceWorktreeScript(options = {}) {
25
- const projectRoot = options.projectRoot ?? process.cwd();
26
- let worktreesDirSegment = DEFAULT_WORKTREES_DIR_SEGMENT;
27
- let wuAllowlistPrefix = `${DEFAULT_WU_ALLOWLIST_PREFIX}/`;
28
- try {
29
- const wuPaths = createWuPaths({ projectRoot });
30
- worktreesDirSegment = normalizeDirectorySegment(
31
- wuPaths.WORKTREES_DIR(),
32
- DEFAULT_WORKTREES_DIR_SEGMENT
33
- );
34
- wuAllowlistPrefix = ensureTrailingSlash(
35
- normalizeDirectorySegment(wuPaths.WU_DIR(), DEFAULT_WU_ALLOWLIST_PREFIX)
36
- );
37
- } catch {
38
- }
39
- return `#!/bin/bash
40
- #
41
- # enforce-worktree.sh (WU-1367, WU-1501)
42
- #
43
- # PreToolUse hook that blocks Write/Edit on main checkout.
44
- # WU-1501: Fail-closed - blocks even when no worktrees exist.
45
- # Graceful degradation only when LumenFlow is NOT configured.
46
- #
47
- # Allowlist: ${wuAllowlistPrefix}, .lumenflow/, .claude/, plan/
48
- # Branch-PR claimed_mode permits writes from main checkout.
49
- #
50
- # Exit codes:
51
- # 0 = Allow operation
52
- # 2 = Block operation (stderr shown to Claude as guidance)
53
- #
54
-
55
- set -euo pipefail
56
-
57
- SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
58
-
59
- # Graceful degradation: if we can't determine state, allow the operation
60
- graceful_allow() {
61
- local reason="$1"
62
- exit 0
63
- }
64
-
65
- # Derive repo paths from CLAUDE_PROJECT_DIR
66
- if [[ -z "\${CLAUDE_PROJECT_DIR:-}" ]]; then
67
- graceful_allow "CLAUDE_PROJECT_DIR not set"
68
- fi
69
-
70
- MAIN_REPO_PATH="$CLAUDE_PROJECT_DIR"
71
- WORKTREES_DIR="\${MAIN_REPO_PATH}/${worktreesDirSegment}"
72
- LUMENFLOW_DIR="\${MAIN_REPO_PATH}/.lumenflow"
73
-
74
- # Check if .lumenflow exists (LumenFlow is configured)
75
- if [[ ! -d "$LUMENFLOW_DIR" ]]; then
76
- graceful_allow "No .lumenflow directory (LumenFlow not configured)"
77
- fi
78
-
79
- # Read JSON input from stdin
80
- INPUT=$(cat)
81
-
82
- if [[ -z "$INPUT" ]]; then
83
- graceful_allow "No input provided"
84
- fi
85
-
86
- # Parse JSON with Python
87
- TMPFILE=$(mktemp)
88
- echo "$INPUT" > "$TMPFILE"
89
-
90
- PARSE_RESULT=$(python3 -c "
91
- import json
92
- import sys
93
- try:
94
- with open('$TMPFILE', 'r') as f:
95
- data = json.load(f)
96
- tool_name = data.get('tool_name', '')
97
- tool_input = data.get('tool_input', {})
98
- if not isinstance(tool_input, dict):
99
- tool_input = {}
100
- file_path = tool_input.get('file_path', '')
101
- print('OK')
102
- print(tool_name if tool_name else '')
103
- print(file_path if file_path else '')
104
- except Exception as e:
105
- print('ERROR')
106
- print(str(e))
107
- print('')
108
- " 2>&1)
109
-
110
- rm -f "$TMPFILE"
111
-
112
- # Parse the result
113
- PARSE_STATUS=$(echo "$PARSE_RESULT" | head -1)
114
- TOOL_NAME=$(echo "$PARSE_RESULT" | sed -n '2p')
115
- FILE_PATH=$(echo "$PARSE_RESULT" | sed -n '3p')
116
-
117
- if [[ "$PARSE_STATUS" != "OK" ]]; then
118
- graceful_allow "JSON parse failed"
119
- fi
120
-
121
- # Only process Write and Edit tools
122
- if [[ "$TOOL_NAME" != "Write" && "$TOOL_NAME" != "Edit" ]]; then
123
- exit 0
124
- fi
125
-
126
- if [[ -z "$FILE_PATH" ]]; then
127
- graceful_allow "No file_path in input"
128
- fi
129
-
130
- # Canonicalize tool path before resolution (e.g., "~/" -> "$HOME/")
131
- CANONICAL_PATH="$FILE_PATH"
132
- if [[ "$CANONICAL_PATH" == "~" ]]; then
133
- if [[ -n "\${HOME:-}" ]]; then
134
- CANONICAL_PATH="$HOME"
135
- fi
136
- elif [[ "$CANONICAL_PATH" == "~/"* || "$CANONICAL_PATH" == "~\\\\"* ]]; then
137
- if [[ -n "\${HOME:-}" ]]; then
138
- CANONICAL_PATH="\${HOME}/\${CANONICAL_PATH:2}"
139
- fi
140
- fi
141
-
142
- # Resolve the canonicalized file path
143
- RESOLVED_PATH=$(realpath -m "$CANONICAL_PATH" 2>/dev/null || echo "$CANONICAL_PATH")
144
-
145
- # Allow if path is outside repo entirely
146
- if [[ "$RESOLVED_PATH" != "\${MAIN_REPO_PATH}/"* && "$RESOLVED_PATH" != "\${MAIN_REPO_PATH}" ]]; then
147
- exit 0
148
- fi
149
-
150
- # Allow if path is inside a worktree
151
- if [[ "$RESOLVED_PATH" == "\${WORKTREES_DIR}/"* ]]; then
152
- exit 0
153
- fi
154
-
155
- # Check if UnsafeAny active worktrees exist
156
- WORKTREE_COUNT=0
157
- if [[ -d "$WORKTREES_DIR" ]]; then
158
- WORKTREE_COUNT=$(find "$WORKTREES_DIR" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | wc -l)
159
- fi
160
-
161
- # If worktrees exist, block writes to main repo (original behavior)
162
- if [[ "$WORKTREE_COUNT" -gt 0 ]]; then
163
- ACTIVE_WORKTREES=$(find "$WORKTREES_DIR" -mindepth 1 -maxdepth 1 -type d -printf '%f\\n' 2>/dev/null | head -5 | tr '\\n' ', ' | sed 's/,$//')
164
-
165
- echo "" >&2
166
- echo "=== Worktree Enforcement ===" >&2
167
- echo "" >&2
168
- echo "BLOCKED: $TOOL_NAME to main repo" >&2
169
- echo "" >&2
170
- echo "Active worktrees: \${ACTIVE_WORKTREES:-none detected}" >&2
171
- echo "" >&2
172
- echo "USE INSTEAD:" >&2
173
- echo " 1. cd to your worktree: cd ${worktreesDirSegment}/<lane>-wu-<id>/" >&2
174
- echo " 2. Make your edits in the worktree" >&2
175
- echo "" >&2
176
- echo "See: LUMENFLOW.md for worktree discipline" >&2
177
- echo "==============================" >&2
178
- exit 2
179
- fi
180
-
181
- # WU-1501: Fail-closed on main when no active worktrees exist
182
- # Check allowlist: paths that are always safe to write on main
183
- RELATIVE_PATH="\${RESOLVED_PATH#\${MAIN_REPO_PATH}/}"
184
-
185
- case "$RELATIVE_PATH" in
186
- ${wuAllowlistPrefix}*) exit 0 ;; # WU YAML specs
187
- .lumenflow/*) exit 0 ;; # LumenFlow state/config
188
- .claude/*) exit 0 ;; # Claude Code config
189
- plan/*) exit 0 ;; # Plan/spec scaffolds
190
- esac
191
-
192
- # Check for branch-pr claimed_mode (allows main writes without worktree)
193
- STATE_FILE="\${LUMENFLOW_DIR}/state/wu-events.jsonl"
194
- if [[ -f "$STATE_FILE" ]]; then
195
- if grep -q '"claimed_mode":"branch-pr"' "$STATE_FILE" 2>/dev/null; then
196
- if grep -q '"status":"in_progress"' "$STATE_FILE" 2>/dev/null; then
197
- exit 0 # Branch-PR WU active - allow main writes
198
- fi
199
- fi
200
- fi
201
-
202
- # WU-1501: Fail-closed - no active claim context, block the write
203
- echo "" >&2
204
- echo "=== Worktree Enforcement ===" >&2
205
- echo "" >&2
206
- echo "BLOCKED: $TOOL_NAME on main (no active WU claim)" >&2
207
- echo "" >&2
208
- echo "No worktrees exist and no branch-pr WU is in progress." >&2
209
- echo "" >&2
210
- echo "WHAT TO DO:" >&2
211
- echo " 1. Claim a WU: pnpm wu:claim --id WU-XXXX --lane \\"<Lane>\\"" >&2
212
- echo " 2. cd ${worktreesDirSegment}/<lane>-wu-xxxx" >&2
213
- echo " 3. Make your edits in the worktree" >&2
214
- echo "" >&2
215
- echo "See: LUMENFLOW.md for worktree discipline" >&2
216
- echo "==============================" >&2
217
- exit 2
218
- `;
219
- }
220
-
221
- // src/hooks/generators/require-wu.ts
222
- function generateRequireWuScript() {
223
- return `#!/bin/bash
224
- #
225
- # require-wu.sh (WU-1367)
226
- #
227
- # PreToolUse hook that blocks Write/Edit when no WU is claimed.
228
- # Graceful degradation: allows operations if state cannot be determined.
229
- #
230
- # Exit codes:
231
- # 0 = Allow operation
232
- # 2 = Block operation (stderr shown to Claude as guidance)
233
- #
234
-
235
- set -euo pipefail
236
-
237
- SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
238
-
239
- # Graceful degradation
240
- graceful_allow() {
241
- local reason="$1"
242
- exit 0
243
- }
244
-
245
- if [[ -z "\${CLAUDE_PROJECT_DIR:-}" ]]; then
246
- graceful_allow "CLAUDE_PROJECT_DIR not set"
247
- fi
248
-
249
- MAIN_REPO_PATH="$CLAUDE_PROJECT_DIR"
250
- WORKTREES_DIR="\${MAIN_REPO_PATH}/worktrees"
251
- LUMENFLOW_DIR="\${MAIN_REPO_PATH}/.lumenflow"
252
- STATE_FILE="\${LUMENFLOW_DIR}/state/wu-events.jsonl"
253
-
254
- # Check if LumenFlow is configured
255
- if [[ ! -d "$LUMENFLOW_DIR" ]]; then
256
- graceful_allow "No .lumenflow directory"
257
- fi
258
-
259
- # Read JSON input
260
- INPUT=$(cat)
261
- if [[ -z "$INPUT" ]]; then
262
- graceful_allow "No input"
263
- fi
264
-
265
- # Parse JSON
266
- TMPFILE=$(mktemp)
267
- echo "$INPUT" > "$TMPFILE"
268
-
269
- PARSE_RESULT=$(python3 -c "
270
- import json
271
- try:
272
- with open('$TMPFILE', 'r') as f:
273
- data = json.load(f)
274
- tool_name = data.get('tool_name', '')
275
- tool_input = data.get('tool_input', {})
276
- if not isinstance(tool_input, dict):
277
- tool_input = {}
278
- file_path = tool_input.get('file_path', '')
279
- print('OK')
280
- print(tool_name if tool_name else '')
281
- print(file_path if file_path else '')
282
- except:
283
- print('ERROR')
284
- print('')
285
- print('')
286
- " 2>/dev/null || echo "ERROR")
287
-
288
- rm -f "$TMPFILE"
289
-
290
- # Parse result
291
- PARSE_STATUS=$(echo "$PARSE_RESULT" | head -1)
292
- TOOL_NAME=$(echo "$PARSE_RESULT" | sed -n '2p')
293
- FILE_PATH=$(echo "$PARSE_RESULT" | sed -n '3p')
294
-
295
- if [[ "$PARSE_STATUS" != "OK" ]]; then
296
- graceful_allow "JSON parse failed"
297
- fi
298
-
299
- # Only check Write and Edit
300
- if [[ "$TOOL_NAME" != "Write" && "$TOOL_NAME" != "Edit" ]]; then
301
- exit 0
302
- fi
303
-
304
- if [[ -z "$FILE_PATH" ]]; then
305
- graceful_allow "No file_path in input"
306
- fi
307
-
308
- # Canonicalize tool path before resolution (e.g., "~/" -> "$HOME/")
309
- CANONICAL_PATH="$FILE_PATH"
310
- if [[ "$CANONICAL_PATH" == "~" ]]; then
311
- if [[ -n "\${HOME:-}" ]]; then
312
- CANONICAL_PATH="$HOME"
313
- fi
314
- elif [[ "$CANONICAL_PATH" == "~/"* || "$CANONICAL_PATH" == "~\\\\"* ]]; then
315
- if [[ -n "\${HOME:-}" ]]; then
316
- CANONICAL_PATH="\${HOME}/\${CANONICAL_PATH:2}"
317
- fi
318
- fi
319
-
320
- # Resolve the canonicalized file path
321
- RESOLVED_PATH=$(realpath -m "$CANONICAL_PATH" 2>/dev/null || echo "$CANONICAL_PATH")
322
-
323
- # Only enforce WU requirement for writes targeting this repository
324
- if [[ "$RESOLVED_PATH" != "\${MAIN_REPO_PATH}/"* && "$RESOLVED_PATH" != "\${MAIN_REPO_PATH}" ]]; then
325
- exit 0
326
- fi
327
-
328
- # Check for active worktrees (indicates claimed WU)
329
- if [[ -d "$WORKTREES_DIR" ]]; then
330
- WORKTREE_COUNT=$(find "$WORKTREES_DIR" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | wc -l)
331
- if [[ "$WORKTREE_COUNT" -gt 0 ]]; then
332
- exit 0 # Has worktrees = has claimed WU
333
- fi
334
- fi
335
-
336
- # Check state file for in_progress WUs
337
- if [[ -f "$STATE_FILE" ]]; then
338
- # Look for UnsafeAny WU with in_progress status
339
- if grep -q '"status":"in_progress"' "$STATE_FILE" 2>/dev/null; then
340
- exit 0 # Has in_progress WU
341
- fi
342
- fi
343
-
344
- # No claimed WU found
345
- echo "" >&2
346
- echo "=== WU Enforcement ===" >&2
347
- echo "" >&2
348
- echo "BLOCKED: $TOOL_NAME without claimed WU" >&2
349
- echo "" >&2
350
- echo "You must claim a WU before making edits:" >&2
351
- echo " pnpm wu:claim --id WU-XXXX --lane <Lane>" >&2
352
- echo " cd worktrees/<lane>-wu-xxxx" >&2
353
- echo "" >&2
354
- echo "Or create a new WU:" >&2
355
- echo " pnpm wu:create --lane <Lane> --title "Description"" >&2
356
- echo "" >&2
357
- echo "See: LUMENFLOW.md for workflow details" >&2
358
- echo "======================" >&2
359
- exit 2
360
- `;
361
- }
362
-
363
- // src/hooks/generators/warn-incomplete.ts
364
- function generateWarnIncompleteScript() {
365
- return `#!/bin/bash
366
- #
367
- # warn-incomplete.sh (WU-1367)
368
- #
369
- # Stop hook that warns when session ends without wu:done.
370
- # This is advisory only - never blocks session termination.
371
- #
372
- # Exit codes:
373
- # 0 = Always (warnings only)
374
- #
375
-
376
- SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
377
-
378
- if [[ -z "\${CLAUDE_PROJECT_DIR:-}" ]]; then
379
- exit 0
380
- fi
381
-
382
- MAIN_REPO_PATH="$CLAUDE_PROJECT_DIR"
383
- WORKTREES_DIR="\${MAIN_REPO_PATH}/worktrees"
384
-
385
- # Check for active worktrees
386
- if [[ ! -d "$WORKTREES_DIR" ]]; then
387
- exit 0
388
- fi
389
-
390
- WORKTREE_COUNT=$(find "$WORKTREES_DIR" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | wc -l)
391
- if [[ "$WORKTREE_COUNT" -eq 0 ]]; then
392
- exit 0
393
- fi
394
-
395
- # Get active worktree names
396
- ACTIVE_WORKTREES=$(find "$WORKTREES_DIR" -mindepth 1 -maxdepth 1 -type d -printf '%f\\n' 2>/dev/null | head -5 | tr '\\n' ', ' | sed 's/,$//')
397
-
398
- echo "" >&2
399
- echo "=== Session Completion Reminder ===" >&2
400
- echo "" >&2
401
- echo "You have active worktrees: $ACTIVE_WORKTREES" >&2
402
- echo "" >&2
403
- echo "If your work is complete, remember to run:" >&2
404
- echo " pnpm wu:prep --id WU-XXXX (from worktree)" >&2
405
- echo " pnpm wu:done --id WU-XXXX (from main)" >&2
406
- echo "" >&2
407
- echo "If work is incomplete, it will be preserved in the worktree." >&2
408
- echo "====================================" >&2
409
-
410
- exit 0
411
- `;
412
- }
413
-
414
- // src/hooks/generators/session-start-recovery.ts
415
- function generateSessionStartRecoveryScript() {
416
- return `#!/bin/bash
417
- #
418
- # session-start-recovery.sh
419
- #
420
- # SessionStart hook - check for pending recovery and inject context (WU-1390)
421
- #
422
- # Fires after session start (on compact, resume, or clear) to:
423
- # 1. Check for recovery-pending-*.md files written by pre-compact-checkpoint.sh
424
- # 2. Display the recovery context to the agent
425
- # 3. Remove the recovery file (one-time recovery)
426
- #
427
- # This completes the durable recovery pattern:
428
- # PreCompact writes file \u2192 SessionStart reads and deletes it
429
- #
430
- # Exit codes:
431
- # 0 = Always allow (informational hook)
432
- #
433
-
434
- set -euo pipefail
435
-
436
- # Derive repo paths from CLAUDE_PROJECT_DIR
437
- if [[ -n "\${CLAUDE_PROJECT_DIR:-}" ]]; then
438
- REPO_PATH="$CLAUDE_PROJECT_DIR"
439
- else
440
- REPO_PATH=$(git rev-parse --show-toplevel 2>/dev/null || echo "")
441
- if [[ -z "$REPO_PATH" ]]; then
442
- exit 0
443
- fi
444
- fi
445
-
446
- RECOVERY_DIR="\${REPO_PATH}/.lumenflow/state"
447
-
448
- # Check if recovery directory exists
449
- if [[ ! -d "$RECOVERY_DIR" ]]; then
450
- exit 0
451
- fi
452
-
453
- # Find UnsafeAny pending recovery files
454
- FOUND_RECOVERY=false
455
-
456
- for recovery_file in "$RECOVERY_DIR"/recovery-pending-*.md; do
457
- # Check if glob matched UnsafeAny files (bash glob returns literal pattern if no match)
458
- [[ -f "$recovery_file" ]] || continue
459
-
460
- FOUND_RECOVERY=true
461
-
462
- # Extract WU ID from filename for display
463
- WU_ID=$(basename "$recovery_file" | sed 's/recovery-pending-\\(.*\\)\\.md/\\1/')
464
-
465
- echo "" >&2
466
- echo "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550" >&2
467
- echo "\u26A0\uFE0F POST-COMPACTION RECOVERY DETECTED" >&2
468
- echo "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550" >&2
469
- echo "" >&2
470
-
471
- # Display the recovery context
472
- cat "$recovery_file" >&2
473
-
474
- echo "" >&2
475
- echo "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550" >&2
476
- echo "" >&2
477
-
478
- # Remove after displaying (one-time recovery)
479
- rm -f "$recovery_file"
480
- done
481
-
482
- # Additional context if recovery was displayed
483
- if [[ "$FOUND_RECOVERY" == "true" ]]; then
484
- echo "IMPORTANT: Your context was compacted. Review the recovery info above." >&2
485
- echo "Recommended: Run 'pnpm wu:brief --id $WU_ID --client \${LUMENFLOW_CLIENT:-claude-code}' for fresh full context." >&2
486
- echo "" >&2
487
- fi
488
-
489
- # WU-1473: Surface unread coordination signals for non-worktree orchestrators
490
- # Even without recovery files, agents benefit from seeing recent inbox activity
491
- # This supports orchestrators running from main checkout (not in a worktree)
492
- pnpm mem:inbox --since 1h --unread-only --quiet 2>/dev/null >&2 || true
493
-
494
- exit 0
495
- `;
496
- }
497
-
498
- // src/hooks/generators/signal-utils.ts
499
- async function surfaceUnreadSignals(baseDir) {
500
- try {
501
- const signals = await loadSignals(baseDir, { unreadOnly: true });
502
- return { count: signals.length, signals };
503
- } catch {
504
- return { count: 0, signals: [] };
505
- }
506
- }
507
- async function markCompletedWUSignalsAsRead(baseDir, wuId) {
508
- try {
509
- const signals = await loadSignals(baseDir, { wuId, unreadOnly: true });
510
- if (signals.length === 0) {
511
- return { markedCount: 0 };
512
- }
513
- const signalIds = signals.map((sig) => sig.id);
514
- return await markSignalsAsRead(baseDir, signalIds);
515
- } catch {
516
- return { markedCount: 0 };
517
- }
518
- }
519
-
520
- // src/hooks/enforcement-generator.ts
521
- var HOOK_SCRIPTS = CLAUDE_HOOKS.SCRIPTS;
522
- function generateEnforcementHooks(config) {
523
- const hooks = {};
524
- const preToolUseHooks = [];
525
- if (config.block_outside_worktree || config.require_wu_for_edits) {
526
- const writeEditHooks = [];
527
- if (config.block_outside_worktree) {
528
- writeEditHooks.push({
529
- type: "command",
530
- command: getHookCommand(HOOK_SCRIPTS.ENFORCE_WORKTREE)
531
- });
532
- }
533
- if (config.require_wu_for_edits) {
534
- writeEditHooks.push({
535
- type: "command",
536
- command: getHookCommand(HOOK_SCRIPTS.REQUIRE_WU)
537
- });
538
- }
539
- if (writeEditHooks.length > 0) {
540
- preToolUseHooks.push({
541
- matcher: CLAUDE_HOOKS.MATCHERS.WRITE_EDIT,
542
- hooks: writeEditHooks
543
- });
544
- }
545
- }
546
- if (preToolUseHooks.length > 0) {
547
- hooks.preToolUse = preToolUseHooks;
548
- }
549
- if (config.warn_on_stop_without_wu_done) {
550
- hooks.stop = [
551
- {
552
- matcher: CLAUDE_HOOKS.MATCHERS.ALL,
553
- hooks: [
554
- {
555
- type: "command",
556
- command: getHookCommand(HOOK_SCRIPTS.WARN_INCOMPLETE)
557
- }
558
- ]
559
- }
560
- ];
561
- }
562
- const postToolUseHooks = [];
563
- if (config.auto_checkpoint?.enabled) {
564
- const autoCheckpointHook = {
565
- type: "command",
566
- command: getHookCommand(HOOK_SCRIPTS.AUTO_CHECKPOINT)
567
- };
568
- postToolUseHooks.push({
569
- matcher: CLAUDE_HOOKS.MATCHERS.ALL,
570
- hooks: [autoCheckpointHook]
571
- });
572
- hooks.subagentStop = [
573
- {
574
- matcher: CLAUDE_HOOKS.MATCHERS.SUBAGENT_STOP,
575
- hooks: [autoCheckpointHook]
576
- }
577
- ];
578
- }
579
- if (postToolUseHooks.length > 0) {
580
- hooks.postToolUse = postToolUseHooks;
581
- }
582
- hooks.preCompact = [
583
- {
584
- matcher: CLAUDE_HOOKS.MATCHERS.ALL,
585
- hooks: [
586
- {
587
- type: "command",
588
- command: getHookCommand(HOOK_SCRIPTS.PRE_COMPACT_CHECKPOINT)
589
- }
590
- ]
591
- }
592
- ];
593
- hooks.sessionStart = [
594
- {
595
- matcher: CLAUDE_HOOKS.MATCHERS.COMPACT,
596
- hooks: [
597
- {
598
- type: "command",
599
- command: getHookCommand(HOOK_SCRIPTS.SESSION_START_RECOVERY)
600
- }
601
- ]
602
- },
603
- {
604
- matcher: CLAUDE_HOOKS.MATCHERS.RESUME,
605
- hooks: [
606
- {
607
- type: "command",
608
- command: getHookCommand(HOOK_SCRIPTS.SESSION_START_RECOVERY)
609
- }
610
- ]
611
- },
612
- {
613
- matcher: CLAUDE_HOOKS.MATCHERS.CLEAR,
614
- hooks: [
615
- {
616
- type: "command",
617
- command: getHookCommand(HOOK_SCRIPTS.SESSION_START_RECOVERY)
618
- }
619
- ]
620
- }
621
- ];
622
- return hooks;
623
- }
624
-
625
- export {
626
- generateEnforceWorktreeScript,
627
- generateRequireWuScript,
628
- generateWarnIncompleteScript,
629
- generateSessionStartRecoveryScript,
630
- surfaceUnreadSignals,
631
- markCompletedWUSignalsAsRead,
632
- generateEnforcementHooks
633
- };