@lumenflow/cli 3.12.4 → 3.12.6
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/dist/chunk-2D2VOCA4.js +37 -0
- package/dist/chunk-2D5KFYGX.js +284 -0
- package/dist/chunk-2GXVIN57.js +14072 -0
- package/dist/chunk-2MQ7HZWZ.js +26 -0
- package/dist/chunk-2UFQ3A3C.js +643 -0
- package/dist/chunk-3RG5ZIWI.js +10 -0
- package/dist/chunk-4N74J3UT.js +15 -0
- package/dist/chunk-5GTOXFYR.js +392 -0
- package/dist/chunk-5VY6MQMC.js +240 -0
- package/dist/chunk-67XVPMRY.js +1297 -0
- package/dist/chunk-6HO4GWJE.js +164 -0
- package/dist/chunk-6W5XHWYV.js +1890 -0
- package/dist/chunk-6X4EMYJQ.js +64 -0
- package/dist/chunk-6XYXI2NQ.js +772 -0
- package/dist/chunk-7ANSOV6Q.js +285 -0
- package/dist/chunk-A624LFLB.js +1380 -0
- package/dist/chunk-ADN5NHG4.js +126 -0
- package/dist/chunk-B7YJYJKG.js +33 -0
- package/dist/chunk-CCLHCPKG.js +210 -0
- package/dist/chunk-CK36VROC.js +1584 -0
- package/dist/chunk-D3UOFRSB.js +81 -0
- package/dist/chunk-DFR4DJBM.js +230 -0
- package/dist/chunk-DSYBDHYH.js +79 -0
- package/dist/chunk-DWMLTXKQ.js +1176 -0
- package/dist/chunk-E3REJTAJ.js +28 -0
- package/dist/chunk-EA3IVO64.js +633 -0
- package/dist/chunk-EK2AKZKD.js +55 -0
- package/dist/chunk-ELD7JTTT.js +343 -0
- package/dist/chunk-EX6TT2XI.js +195 -0
- package/dist/chunk-EXINSFZE.js +82 -0
- package/dist/chunk-EZ6ZBYBM.js +510 -0
- package/dist/chunk-FBKAPTJ2.js +16 -0
- package/dist/chunk-FVLV5RYH.js +1118 -0
- package/dist/chunk-GDNSBQVK.js +2485 -0
- package/dist/chunk-GPQHMBNN.js +278 -0
- package/dist/chunk-GTFJB67L.js +68 -0
- package/dist/chunk-HANJXVKW.js +1127 -0
- package/dist/chunk-HEVS5YLD.js +269 -0
- package/dist/chunk-HMEVZKPQ.js +9 -0
- package/dist/chunk-HRGSYNLM.js +3511 -0
- package/dist/chunk-ISZR5N4K.js +60 -0
- package/dist/chunk-J6SUPR2C.js +226 -0
- package/dist/chunk-JERYVEIZ.js +244 -0
- package/dist/chunk-JHHWGL2N.js +87 -0
- package/dist/chunk-JONWQUB5.js +775 -0
- package/dist/chunk-K2DIWWDM.js +1766 -0
- package/dist/chunk-KY4PGL5V.js +969 -0
- package/dist/chunk-L737LQ4C.js +1285 -0
- package/dist/chunk-LFTWYIB2.js +497 -0
- package/dist/chunk-LV47RFNJ.js +41 -0
- package/dist/chunk-MKSAITI7.js +15 -0
- package/dist/chunk-MZ7RKIX4.js +212 -0
- package/dist/chunk-NAP6CFSO.js +84 -0
- package/dist/chunk-ND6MY37M.js +16 -0
- package/dist/chunk-NMG736UR.js +683 -0
- package/dist/chunk-NRAXROED.js +32 -0
- package/dist/chunk-NRIZR3A7.js +690 -0
- package/dist/chunk-NX43BG3M.js +233 -0
- package/dist/chunk-O645XLSI.js +297 -0
- package/dist/chunk-OMJD6A3S.js +235 -0
- package/dist/chunk-QB6SJD4T.js +430 -0
- package/dist/chunk-QFSTL4J3.js +276 -0
- package/dist/chunk-QLGDFMFX.js +212 -0
- package/dist/chunk-RIAAGL2E.js +13 -0
- package/dist/chunk-RWO5XMZ6.js +86 -0
- package/dist/chunk-RXRKBBSM.js +149 -0
- package/dist/chunk-RZOZMML6.js +363 -0
- package/dist/chunk-U7I7FS7T.js +113 -0
- package/dist/chunk-UI42RODY.js +717 -0
- package/dist/chunk-UTVMVSCO.js +519 -0
- package/dist/chunk-V6OJGLBA.js +1746 -0
- package/dist/chunk-W2JHVH7D.js +152 -0
- package/dist/chunk-WD3Y7VQN.js +280 -0
- package/dist/chunk-WOCTQ5MS.js +303 -0
- package/dist/chunk-WZR3ZUNN.js +696 -0
- package/dist/chunk-XGI665H7.js +150 -0
- package/dist/chunk-XKY65P2T.js +304 -0
- package/dist/chunk-Y4CQZY65.js +57 -0
- package/dist/chunk-YFEXKLVE.js +194 -0
- package/dist/chunk-YHO3HS5X.js +287 -0
- package/dist/chunk-YLS7AZSX.js +738 -0
- package/dist/chunk-ZE473AO6.js +49 -0
- package/dist/chunk-ZF747T3O.js +644 -0
- package/dist/chunk-ZHCZHZH3.js +43 -0
- package/dist/chunk-ZZNZX2XY.js +87 -0
- package/dist/constants-7QAP3VQ4.js +23 -0
- package/dist/dist-IY3UUMWK.js +33 -0
- package/dist/init-templates.js +9 -9
- package/dist/invariants-runner-W5RGHCSU.js +27 -0
- package/dist/lane-lock-6J36HD5O.js +35 -0
- package/dist/mem-checkpoint-core-EANG2GVN.js +14 -0
- package/dist/mem-signal-core-2LZ2WYHW.js +19 -0
- package/dist/memory-store-OLB5FO7K.js +18 -0
- package/dist/service-6BYCOCO5.js +13 -0
- package/dist/spawn-policy-resolver-NTSZYQ6R.js +17 -0
- package/dist/spawn-task-builder-R4E2BHSW.js +22 -0
- package/dist/wu-claim.js +2 -2
- package/dist/wu-claim.js.map +1 -1
- package/dist/wu-done-already-merged.js +12 -5
- package/dist/wu-done-already-merged.js.map +1 -1
- package/dist/wu-done-gates.js +25 -4
- package/dist/wu-done-gates.js.map +1 -1
- package/dist/wu-done-ownership.js +6 -1
- package/dist/wu-done-ownership.js.map +1 -1
- package/dist/wu-done-pr-WLFFFEPJ.js +25 -0
- package/dist/wu-done-validation-3J5E36FE.js +30 -0
- package/dist/wu-done.js +6 -6
- package/dist/wu-done.js.map +1 -1
- package/dist/wu-duplicate-id-detector-5S7JHELK.js +232 -0
- package/dist/wu-edit-operations.js +58 -17
- package/dist/wu-edit-operations.js.map +1 -1
- package/dist/wu-edit-validators.js +104 -28
- package/dist/wu-edit-validators.js.map +1 -1
- package/dist/wu-edit.js +1 -1
- package/dist/wu-edit.js.map +1 -1
- package/dist/wu-spawn-prompt-builders.js +8 -7
- package/dist/wu-spawn-prompt-builders.js.map +1 -1
- package/package.json +8 -8
- package/packs/sidekick/.turbo/turbo-build.log +1 -1
- package/packs/sidekick/package.json +1 -1
- package/packs/software-delivery/.turbo/turbo-build.log +1 -1
- package/packs/software-delivery/package.json +1 -1
- package/templates/core/LUMENFLOW.md.template +1 -1
- package/templates/core/ai/onboarding/quick-ref-commands.md.template +3 -3
- package/templates/core/ai/onboarding/starting-prompt.md.template +11 -11
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,633 @@
|
|
|
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
|
+
};
|