@lumenflow/cli 3.12.6 → 3.12.8

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 (111) hide show
  1. package/dist/config-set.js +82 -32
  2. package/dist/config-set.js.map +1 -1
  3. package/dist/wu-claim.js +2 -1
  4. package/dist/wu-claim.js.map +1 -1
  5. package/dist/wu-done-policies.js +9 -9
  6. package/dist/wu-done-policies.js.map +1 -1
  7. package/dist/wu-spawn-strategy-resolver.js +14 -6
  8. package/dist/wu-spawn-strategy-resolver.js.map +1 -1
  9. package/package.json +8 -8
  10. package/packs/sidekick/.turbo/turbo-build.log +1 -1
  11. package/packs/sidekick/package.json +1 -1
  12. package/packs/software-delivery/.turbo/turbo-build.log +1 -1
  13. package/packs/software-delivery/package.json +1 -1
  14. package/dist/chunk-2D2VOCA4.js +0 -37
  15. package/dist/chunk-2D5KFYGX.js +0 -284
  16. package/dist/chunk-2GXVIN57.js +0 -14072
  17. package/dist/chunk-2MQ7HZWZ.js +0 -26
  18. package/dist/chunk-2UFQ3A3C.js +0 -643
  19. package/dist/chunk-3RG5ZIWI.js +0 -10
  20. package/dist/chunk-4N74J3UT.js +0 -15
  21. package/dist/chunk-5GTOXFYR.js +0 -392
  22. package/dist/chunk-5VY6MQMC.js +0 -240
  23. package/dist/chunk-67XVPMRY.js +0 -1297
  24. package/dist/chunk-6HO4GWJE.js +0 -164
  25. package/dist/chunk-6W5XHWYV.js +0 -1890
  26. package/dist/chunk-6X4EMYJQ.js +0 -64
  27. package/dist/chunk-6XYXI2NQ.js +0 -772
  28. package/dist/chunk-7ANSOV6Q.js +0 -285
  29. package/dist/chunk-A624LFLB.js +0 -1380
  30. package/dist/chunk-ADN5NHG4.js +0 -126
  31. package/dist/chunk-B7YJYJKG.js +0 -33
  32. package/dist/chunk-CCLHCPKG.js +0 -210
  33. package/dist/chunk-CK36VROC.js +0 -1584
  34. package/dist/chunk-D3UOFRSB.js +0 -81
  35. package/dist/chunk-DFR4DJBM.js +0 -230
  36. package/dist/chunk-DSYBDHYH.js +0 -79
  37. package/dist/chunk-DWMLTXKQ.js +0 -1176
  38. package/dist/chunk-E3REJTAJ.js +0 -28
  39. package/dist/chunk-EA3IVO64.js +0 -633
  40. package/dist/chunk-EK2AKZKD.js +0 -55
  41. package/dist/chunk-ELD7JTTT.js +0 -343
  42. package/dist/chunk-EX6TT2XI.js +0 -195
  43. package/dist/chunk-EXINSFZE.js +0 -82
  44. package/dist/chunk-EZ6ZBYBM.js +0 -510
  45. package/dist/chunk-FBKAPTJ2.js +0 -16
  46. package/dist/chunk-FVLV5RYH.js +0 -1118
  47. package/dist/chunk-GDNSBQVK.js +0 -2485
  48. package/dist/chunk-GPQHMBNN.js +0 -278
  49. package/dist/chunk-GTFJB67L.js +0 -68
  50. package/dist/chunk-HANJXVKW.js +0 -1127
  51. package/dist/chunk-HEVS5YLD.js +0 -269
  52. package/dist/chunk-HMEVZKPQ.js +0 -9
  53. package/dist/chunk-HRGSYNLM.js +0 -3511
  54. package/dist/chunk-ISZR5N4K.js +0 -60
  55. package/dist/chunk-J6SUPR2C.js +0 -226
  56. package/dist/chunk-JERYVEIZ.js +0 -244
  57. package/dist/chunk-JHHWGL2N.js +0 -87
  58. package/dist/chunk-JONWQUB5.js +0 -775
  59. package/dist/chunk-K2DIWWDM.js +0 -1766
  60. package/dist/chunk-KY4PGL5V.js +0 -969
  61. package/dist/chunk-L737LQ4C.js +0 -1285
  62. package/dist/chunk-LFTWYIB2.js +0 -497
  63. package/dist/chunk-LV47RFNJ.js +0 -41
  64. package/dist/chunk-MKSAITI7.js +0 -15
  65. package/dist/chunk-MZ7RKIX4.js +0 -212
  66. package/dist/chunk-NAP6CFSO.js +0 -84
  67. package/dist/chunk-ND6MY37M.js +0 -16
  68. package/dist/chunk-NMG736UR.js +0 -683
  69. package/dist/chunk-NRAXROED.js +0 -32
  70. package/dist/chunk-NRIZR3A7.js +0 -690
  71. package/dist/chunk-NX43BG3M.js +0 -233
  72. package/dist/chunk-O645XLSI.js +0 -297
  73. package/dist/chunk-OMJD6A3S.js +0 -235
  74. package/dist/chunk-QB6SJD4T.js +0 -430
  75. package/dist/chunk-QFSTL4J3.js +0 -276
  76. package/dist/chunk-QLGDFMFX.js +0 -212
  77. package/dist/chunk-RIAAGL2E.js +0 -13
  78. package/dist/chunk-RWO5XMZ6.js +0 -86
  79. package/dist/chunk-RXRKBBSM.js +0 -149
  80. package/dist/chunk-RZOZMML6.js +0 -363
  81. package/dist/chunk-U7I7FS7T.js +0 -113
  82. package/dist/chunk-UI42RODY.js +0 -717
  83. package/dist/chunk-UTVMVSCO.js +0 -519
  84. package/dist/chunk-V6OJGLBA.js +0 -1746
  85. package/dist/chunk-W2JHVH7D.js +0 -152
  86. package/dist/chunk-WD3Y7VQN.js +0 -280
  87. package/dist/chunk-WOCTQ5MS.js +0 -303
  88. package/dist/chunk-WZR3ZUNN.js +0 -696
  89. package/dist/chunk-XGI665H7.js +0 -150
  90. package/dist/chunk-XKY65P2T.js +0 -304
  91. package/dist/chunk-Y4CQZY65.js +0 -57
  92. package/dist/chunk-YFEXKLVE.js +0 -194
  93. package/dist/chunk-YHO3HS5X.js +0 -287
  94. package/dist/chunk-YLS7AZSX.js +0 -738
  95. package/dist/chunk-ZE473AO6.js +0 -49
  96. package/dist/chunk-ZF747T3O.js +0 -644
  97. package/dist/chunk-ZHCZHZH3.js +0 -43
  98. package/dist/chunk-ZZNZX2XY.js +0 -87
  99. package/dist/constants-7QAP3VQ4.js +0 -23
  100. package/dist/dist-IY3UUMWK.js +0 -33
  101. package/dist/invariants-runner-W5RGHCSU.js +0 -27
  102. package/dist/lane-lock-6J36HD5O.js +0 -35
  103. package/dist/mem-checkpoint-core-EANG2GVN.js +0 -14
  104. package/dist/mem-signal-core-2LZ2WYHW.js +0 -19
  105. package/dist/memory-store-OLB5FO7K.js +0 -18
  106. package/dist/service-6BYCOCO5.js +0 -13
  107. package/dist/spawn-policy-resolver-NTSZYQ6R.js +0 -17
  108. package/dist/spawn-task-builder-R4E2BHSW.js +0 -22
  109. package/dist/wu-done-pr-WLFFFEPJ.js +0 -25
  110. package/dist/wu-done-validation-3J5E36FE.js +0 -30
  111. package/dist/wu-duplicate-id-detector-5S7JHELK.js +0 -232
@@ -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
- };