@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.
- package/README.md +121 -105
- package/dist/__tests__/agent-spawn-coordination.test.js +451 -0
- package/dist/__tests__/commands/integrate.test.js +165 -0
- package/dist/__tests__/commands.test.js +75 -0
- package/dist/__tests__/doctor.test.js +510 -0
- package/dist/__tests__/gates-config.test.js +0 -1
- package/dist/__tests__/hooks/enforcement.test.js +279 -0
- package/dist/__tests__/init-greenfield.test.js +247 -0
- package/dist/__tests__/init-quick-ref.test.js +0 -1
- package/dist/__tests__/init-template-portability.test.js +0 -1
- package/dist/__tests__/init.test.js +249 -0
- package/dist/__tests__/initiative-e2e.test.js +442 -0
- package/dist/__tests__/initiative-plan-replacement.test.js +0 -1
- package/dist/__tests__/memory-integration.test.js +333 -0
- package/dist/__tests__/release.test.js +1 -1
- package/dist/__tests__/safe-git.test.js +0 -1
- package/dist/__tests__/state-doctor.test.js +54 -0
- package/dist/__tests__/sync-templates.test.js +255 -0
- package/dist/__tests__/wu-create-required-fields.test.js +121 -0
- package/dist/__tests__/wu-done-auto-cleanup.test.js +135 -0
- package/dist/__tests__/wu-lifecycle-integration.test.js +388 -0
- package/dist/backlog-prune.js +0 -1
- package/dist/cli-entry-point.js +0 -1
- package/dist/commands/integrate.js +229 -0
- package/dist/commands.js +171 -0
- package/dist/docs-sync.js +46 -0
- package/dist/doctor.js +479 -10
- package/dist/gates.js +0 -7
- package/dist/hooks/enforcement-checks.js +209 -0
- package/dist/hooks/enforcement-generator.js +365 -0
- package/dist/hooks/enforcement-sync.js +243 -0
- package/dist/hooks/index.js +7 -0
- package/dist/init.js +502 -17
- package/dist/initiative-add-wu.js +0 -2
- package/dist/initiative-create.js +0 -3
- package/dist/initiative-edit.js +0 -5
- package/dist/initiative-plan.js +0 -1
- package/dist/initiative-remove-wu.js +0 -2
- package/dist/lane-health.js +0 -2
- package/dist/lane-suggest.js +0 -1
- package/dist/mem-checkpoint.js +0 -2
- package/dist/mem-cleanup.js +0 -2
- package/dist/mem-context.js +0 -3
- package/dist/mem-create.js +0 -2
- package/dist/mem-delete.js +0 -3
- package/dist/mem-inbox.js +0 -2
- package/dist/mem-index.js +0 -1
- package/dist/mem-init.js +0 -2
- package/dist/mem-profile.js +0 -1
- package/dist/mem-promote.js +0 -1
- package/dist/mem-ready.js +0 -2
- package/dist/mem-signal.js +0 -2
- package/dist/mem-start.js +0 -2
- package/dist/mem-summarize.js +0 -2
- package/dist/metrics-cli.js +1 -1
- package/dist/metrics-snapshot.js +1 -1
- package/dist/onboarding-smoke-test.js +0 -5
- package/dist/orchestrate-init-status.js +0 -1
- package/dist/orchestrate-initiative.js +0 -1
- package/dist/orchestrate-monitor.js +0 -1
- package/dist/plan-create.js +0 -2
- package/dist/plan-edit.js +0 -2
- package/dist/plan-link.js +0 -2
- package/dist/plan-promote.js +0 -2
- package/dist/signal-cleanup.js +0 -4
- package/dist/state-bootstrap.js +0 -1
- package/dist/state-cleanup.js +0 -4
- package/dist/state-doctor-fix.js +5 -8
- package/dist/state-doctor.js +0 -11
- package/dist/sync-templates.js +188 -34
- package/dist/wu-block.js +100 -48
- package/dist/wu-claim.js +1 -22
- package/dist/wu-cleanup.js +0 -1
- package/dist/wu-create.js +0 -2
- package/dist/wu-done-auto-cleanup.js +139 -0
- package/dist/wu-done.js +11 -4
- package/dist/wu-edit.js +0 -12
- package/dist/wu-preflight.js +0 -1
- package/dist/wu-prep.js +0 -1
- package/dist/wu-proto.js +0 -1
- package/dist/wu-spawn.js +0 -3
- package/dist/wu-unblock.js +0 -2
- package/dist/wu-validate.js +0 -1
- package/package.json +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
|
+
}
|