@lumenflow/cli 2.9.0 → 2.11.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 +23 -2
- package/dist/__tests__/gates-integration-tests.test.js +112 -0
- package/dist/__tests__/init.test.js +225 -0
- package/dist/__tests__/safe-git.test.js +4 -4
- package/dist/__tests__/wu-create-required-fields.test.js +22 -0
- package/dist/__tests__/wu-create.test.js +72 -0
- package/dist/gates.js +6 -8
- package/dist/hooks/enforcement-generator.js +256 -5
- package/dist/hooks/enforcement-sync.js +52 -6
- package/dist/init.js +195 -2
- package/dist/mem-recover.js +221 -0
- package/dist/orchestrate-initiative.js +19 -1
- package/dist/state-doctor-fix.js +36 -1
- package/dist/state-doctor.js +10 -6
- package/dist/wu-create.js +37 -15
- package/dist/wu-recover.js +53 -2
- package/dist/wu-spawn.js +2 -2
- package/package.json +6 -6
- package/templates/core/.mcp.json.template +8 -0
- package/templates/core/LUMENFLOW.md.template +24 -0
- package/templates/core/ai/onboarding/first-wu-mistakes.md.template +47 -0
- package/templates/core/ai/onboarding/lumenflow-force-usage.md.template +183 -0
- package/templates/core/ai/onboarding/quick-ref-commands.md.template +68 -55
- package/templates/core/ai/onboarding/release-process.md.template +58 -4
- package/templates/core/ai/onboarding/starting-prompt.md.template +67 -3
- package/templates/core/ai/onboarding/vendor-support.md.template +73 -0
- package/templates/core/scripts/safe-git.template +29 -0
- package/templates/vendors/claude/.claude/hooks/pre-compact-checkpoint.sh +102 -0
- package/templates/vendors/claude/.claude/hooks/session-start-recovery.sh +74 -0
- package/templates/vendors/claude/.claude/settings.json.template +42 -0
- package/templates/vendors/claude/.claude/skills/context-management/SKILL.md.template +23 -6
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
* This module generates hook configurations that can be written to
|
|
6
6
|
* .claude/settings.json to enforce LumenFlow workflow compliance.
|
|
7
7
|
*/
|
|
8
|
+
import { CLAUDE_HOOKS, getHookCommand } from '@lumenflow/core';
|
|
9
|
+
// Re-export for backwards compatibility (WU-1394)
|
|
10
|
+
export const HOOK_SCRIPTS = CLAUDE_HOOKS.SCRIPTS;
|
|
8
11
|
/**
|
|
9
12
|
* Generate enforcement hooks based on configuration.
|
|
10
13
|
*
|
|
@@ -20,18 +23,18 @@ export function generateEnforcementHooks(config) {
|
|
|
20
23
|
if (config.block_outside_worktree) {
|
|
21
24
|
writeEditHooks.push({
|
|
22
25
|
type: 'command',
|
|
23
|
-
command:
|
|
26
|
+
command: getHookCommand(HOOK_SCRIPTS.ENFORCE_WORKTREE),
|
|
24
27
|
});
|
|
25
28
|
}
|
|
26
29
|
if (config.require_wu_for_edits) {
|
|
27
30
|
writeEditHooks.push({
|
|
28
31
|
type: 'command',
|
|
29
|
-
command:
|
|
32
|
+
command: getHookCommand(HOOK_SCRIPTS.REQUIRE_WU),
|
|
30
33
|
});
|
|
31
34
|
}
|
|
32
35
|
if (writeEditHooks.length > 0) {
|
|
33
36
|
preToolUseHooks.push({
|
|
34
|
-
matcher:
|
|
37
|
+
matcher: CLAUDE_HOOKS.MATCHERS.WRITE_EDIT,
|
|
35
38
|
hooks: writeEditHooks,
|
|
36
39
|
});
|
|
37
40
|
}
|
|
@@ -43,16 +46,58 @@ export function generateEnforcementHooks(config) {
|
|
|
43
46
|
if (config.warn_on_stop_without_wu_done) {
|
|
44
47
|
hooks.stop = [
|
|
45
48
|
{
|
|
46
|
-
matcher:
|
|
49
|
+
matcher: CLAUDE_HOOKS.MATCHERS.ALL,
|
|
47
50
|
hooks: [
|
|
48
51
|
{
|
|
49
52
|
type: 'command',
|
|
50
|
-
command:
|
|
53
|
+
command: getHookCommand(HOOK_SCRIPTS.WARN_INCOMPLETE),
|
|
51
54
|
},
|
|
52
55
|
],
|
|
53
56
|
},
|
|
54
57
|
];
|
|
55
58
|
}
|
|
59
|
+
// Always generate PreCompact and SessionStart recovery hooks (WU-1394)
|
|
60
|
+
// These enable durable context recovery after compaction
|
|
61
|
+
hooks.preCompact = [
|
|
62
|
+
{
|
|
63
|
+
matcher: CLAUDE_HOOKS.MATCHERS.ALL,
|
|
64
|
+
hooks: [
|
|
65
|
+
{
|
|
66
|
+
type: 'command',
|
|
67
|
+
command: getHookCommand(HOOK_SCRIPTS.PRE_COMPACT_CHECKPOINT),
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
];
|
|
72
|
+
hooks.sessionStart = [
|
|
73
|
+
{
|
|
74
|
+
matcher: CLAUDE_HOOKS.MATCHERS.COMPACT,
|
|
75
|
+
hooks: [
|
|
76
|
+
{
|
|
77
|
+
type: 'command',
|
|
78
|
+
command: getHookCommand(HOOK_SCRIPTS.SESSION_START_RECOVERY),
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
matcher: CLAUDE_HOOKS.MATCHERS.RESUME,
|
|
84
|
+
hooks: [
|
|
85
|
+
{
|
|
86
|
+
type: 'command',
|
|
87
|
+
command: getHookCommand(HOOK_SCRIPTS.SESSION_START_RECOVERY),
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
matcher: CLAUDE_HOOKS.MATCHERS.CLEAR,
|
|
93
|
+
hooks: [
|
|
94
|
+
{
|
|
95
|
+
type: 'command',
|
|
96
|
+
command: getHookCommand(HOOK_SCRIPTS.SESSION_START_RECOVERY),
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
},
|
|
100
|
+
];
|
|
56
101
|
return hooks;
|
|
57
102
|
}
|
|
58
103
|
/**
|
|
@@ -363,3 +408,209 @@ exit 0
|
|
|
363
408
|
`;
|
|
364
409
|
/* eslint-enable no-useless-escape */
|
|
365
410
|
}
|
|
411
|
+
/**
|
|
412
|
+
* Generate the pre-compact-checkpoint.sh hook script content.
|
|
413
|
+
*
|
|
414
|
+
* This PreCompact hook saves a checkpoint and writes a durable recovery file
|
|
415
|
+
* before context compaction. The recovery file survives compaction and is
|
|
416
|
+
* read by session-start-recovery.sh on the next session start.
|
|
417
|
+
*
|
|
418
|
+
* Part of WU-1394: Durable recovery pattern for context preservation.
|
|
419
|
+
*/
|
|
420
|
+
export function generatePreCompactCheckpointScript() {
|
|
421
|
+
// Note: Shell variable escapes (\$, \") are intentional for the generated bash script
|
|
422
|
+
/* eslint-disable no-useless-escape */
|
|
423
|
+
return `#!/bin/bash
|
|
424
|
+
#
|
|
425
|
+
# pre-compact-checkpoint.sh
|
|
426
|
+
#
|
|
427
|
+
# PreCompact hook - auto-checkpoint + durable recovery marker (WU-1390)
|
|
428
|
+
#
|
|
429
|
+
# Fires before context compaction to:
|
|
430
|
+
# 1. Save a checkpoint with the current WU progress
|
|
431
|
+
# 2. Write a durable recovery file that survives compaction
|
|
432
|
+
#
|
|
433
|
+
# The recovery file is read by session-start-recovery.sh on the next
|
|
434
|
+
# session start (after compact, resume, or clear) to restore context.
|
|
435
|
+
#
|
|
436
|
+
# Exit codes:
|
|
437
|
+
# 0 = Always allow (cannot block compaction)
|
|
438
|
+
#
|
|
439
|
+
# Uses python3 for JSON parsing (consistent with other hooks)
|
|
440
|
+
#
|
|
441
|
+
|
|
442
|
+
set -euo pipefail
|
|
443
|
+
|
|
444
|
+
SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
|
|
445
|
+
|
|
446
|
+
# Derive repo paths from CLAUDE_PROJECT_DIR
|
|
447
|
+
if [[ -n "\${CLAUDE_PROJECT_DIR:-}" ]]; then
|
|
448
|
+
REPO_PATH="\$CLAUDE_PROJECT_DIR"
|
|
449
|
+
else
|
|
450
|
+
REPO_PATH=\$(git rev-parse --show-toplevel 2>/dev/null || echo "")
|
|
451
|
+
if [[ -z "\$REPO_PATH" ]]; then
|
|
452
|
+
exit 0
|
|
453
|
+
fi
|
|
454
|
+
fi
|
|
455
|
+
|
|
456
|
+
# Read JSON input from stdin
|
|
457
|
+
INPUT=\$(cat)
|
|
458
|
+
|
|
459
|
+
# Parse trigger from hook input (defensive - default to "auto")
|
|
460
|
+
# PreCompact provides: { "trigger": "manual" | "auto" }
|
|
461
|
+
TRIGGER=\$(python3 -c "
|
|
462
|
+
import json
|
|
463
|
+
import sys
|
|
464
|
+
try:
|
|
465
|
+
data = json.loads('''\$INPUT''')
|
|
466
|
+
trigger = data.get('trigger', 'auto')
|
|
467
|
+
print(trigger if trigger else 'auto')
|
|
468
|
+
except:
|
|
469
|
+
print('auto')
|
|
470
|
+
" 2>/dev/null || echo "auto")
|
|
471
|
+
|
|
472
|
+
# Get WU ID from worktree context (wu:status --json)
|
|
473
|
+
# Location.worktreeWuId is set when in a worktree
|
|
474
|
+
WU_ID=\$(pnpm wu:status --json 2>/dev/null | python3 -c "
|
|
475
|
+
import json
|
|
476
|
+
import sys
|
|
477
|
+
try:
|
|
478
|
+
data = json.load(sys.stdin)
|
|
479
|
+
location = data.get('location', {})
|
|
480
|
+
wu_id = location.get('worktreeWuId') or ''
|
|
481
|
+
print(wu_id)
|
|
482
|
+
except:
|
|
483
|
+
print('')
|
|
484
|
+
" 2>/dev/null || echo "")
|
|
485
|
+
|
|
486
|
+
# Only proceed if we have a WU ID (working in a worktree)
|
|
487
|
+
if [[ -n "\$WU_ID" ]]; then
|
|
488
|
+
# Save checkpoint with pre-compact trigger
|
|
489
|
+
# Note: This may fail if CLI not built, but that's OK - recovery file is more important
|
|
490
|
+
pnpm mem:checkpoint "Auto: pre-\${TRIGGER}-compaction" --wu "\$WU_ID" --trigger "pre-compact" --quiet 2>/dev/null || true
|
|
491
|
+
|
|
492
|
+
# Write durable recovery marker (survives compaction)
|
|
493
|
+
# This is the key mechanism - file persists and is read by session-start-recovery.sh
|
|
494
|
+
RECOVERY_DIR="\${REPO_PATH}/.lumenflow/state"
|
|
495
|
+
RECOVERY_FILE="\${RECOVERY_DIR}/recovery-pending-\${WU_ID}.md"
|
|
496
|
+
|
|
497
|
+
mkdir -p "\$RECOVERY_DIR"
|
|
498
|
+
|
|
499
|
+
# Generate recovery context using mem:recover
|
|
500
|
+
# The --quiet flag outputs only the recovery context without headers
|
|
501
|
+
pnpm mem:recover --wu "\$WU_ID" --quiet > "\$RECOVERY_FILE" 2>/dev/null || {
|
|
502
|
+
# Fallback minimal recovery if mem:recover fails
|
|
503
|
+
cat > "\$RECOVERY_FILE" << EOF
|
|
504
|
+
# POST-COMPACTION RECOVERY
|
|
505
|
+
|
|
506
|
+
You are resuming work after context compaction. Your previous context was lost.
|
|
507
|
+
**WU:** \${WU_ID}
|
|
508
|
+
|
|
509
|
+
## Next Action
|
|
510
|
+
Run \\\`pnpm wu:spawn --id \${WU_ID}\\\` to spawn a fresh agent with full context.
|
|
511
|
+
EOF
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
# Output brief warning to stderr (may be compacted away, but recovery file persists)
|
|
515
|
+
echo "" >&2
|
|
516
|
+
echo "═══════════════════════════════════════════════════════" >&2
|
|
517
|
+
echo "⚠️ COMPACTION: Checkpoint saved for \${WU_ID}" >&2
|
|
518
|
+
echo "Recovery context: \${RECOVERY_FILE}" >&2
|
|
519
|
+
echo "Next: pnpm wu:spawn --id \${WU_ID}" >&2
|
|
520
|
+
echo "═══════════════════════════════════════════════════════" >&2
|
|
521
|
+
fi
|
|
522
|
+
|
|
523
|
+
# Always exit 0 - cannot block compaction
|
|
524
|
+
exit 0
|
|
525
|
+
`;
|
|
526
|
+
/* eslint-enable no-useless-escape */
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Generate the session-start-recovery.sh hook script content.
|
|
530
|
+
*
|
|
531
|
+
* This SessionStart hook checks for pending recovery files written by
|
|
532
|
+
* pre-compact-checkpoint.sh and displays the recovery context to the agent.
|
|
533
|
+
* After displaying, the recovery file is deleted (one-time recovery).
|
|
534
|
+
*
|
|
535
|
+
* Part of WU-1394: Durable recovery pattern for context preservation.
|
|
536
|
+
*/
|
|
537
|
+
export function generateSessionStartRecoveryScript() {
|
|
538
|
+
// Note: Shell variable escapes (\$, \") are intentional for the generated bash script
|
|
539
|
+
/* eslint-disable no-useless-escape */
|
|
540
|
+
return `#!/bin/bash
|
|
541
|
+
#
|
|
542
|
+
# session-start-recovery.sh
|
|
543
|
+
#
|
|
544
|
+
# SessionStart hook - check for pending recovery and inject context (WU-1390)
|
|
545
|
+
#
|
|
546
|
+
# Fires after session start (on compact, resume, or clear) to:
|
|
547
|
+
# 1. Check for recovery-pending-*.md files written by pre-compact-checkpoint.sh
|
|
548
|
+
# 2. Display the recovery context to the agent
|
|
549
|
+
# 3. Remove the recovery file (one-time recovery)
|
|
550
|
+
#
|
|
551
|
+
# This completes the durable recovery pattern:
|
|
552
|
+
# PreCompact writes file → SessionStart reads and deletes it
|
|
553
|
+
#
|
|
554
|
+
# Exit codes:
|
|
555
|
+
# 0 = Always allow (informational hook)
|
|
556
|
+
#
|
|
557
|
+
|
|
558
|
+
set -euo pipefail
|
|
559
|
+
|
|
560
|
+
# Derive repo paths from CLAUDE_PROJECT_DIR
|
|
561
|
+
if [[ -n "\${CLAUDE_PROJECT_DIR:-}" ]]; then
|
|
562
|
+
REPO_PATH="\$CLAUDE_PROJECT_DIR"
|
|
563
|
+
else
|
|
564
|
+
REPO_PATH=\$(git rev-parse --show-toplevel 2>/dev/null || echo "")
|
|
565
|
+
if [[ -z "\$REPO_PATH" ]]; then
|
|
566
|
+
exit 0
|
|
567
|
+
fi
|
|
568
|
+
fi
|
|
569
|
+
|
|
570
|
+
RECOVERY_DIR="\${REPO_PATH}/.lumenflow/state"
|
|
571
|
+
|
|
572
|
+
# Check if recovery directory exists
|
|
573
|
+
if [[ ! -d "\$RECOVERY_DIR" ]]; then
|
|
574
|
+
exit 0
|
|
575
|
+
fi
|
|
576
|
+
|
|
577
|
+
# Find any pending recovery files
|
|
578
|
+
FOUND_RECOVERY=false
|
|
579
|
+
|
|
580
|
+
for recovery_file in "\$RECOVERY_DIR"/recovery-pending-*.md; do
|
|
581
|
+
# Check if glob matched any files (bash glob returns literal pattern if no match)
|
|
582
|
+
[[ -f "\$recovery_file" ]] || continue
|
|
583
|
+
|
|
584
|
+
FOUND_RECOVERY=true
|
|
585
|
+
|
|
586
|
+
# Extract WU ID from filename for display
|
|
587
|
+
WU_ID=\$(basename "\$recovery_file" | sed 's/recovery-pending-\\(.*\\)\\.md/\\1/')
|
|
588
|
+
|
|
589
|
+
echo "" >&2
|
|
590
|
+
echo "═══════════════════════════════════════════════════════" >&2
|
|
591
|
+
echo "⚠️ POST-COMPACTION RECOVERY DETECTED" >&2
|
|
592
|
+
echo "═══════════════════════════════════════════════════════" >&2
|
|
593
|
+
echo "" >&2
|
|
594
|
+
|
|
595
|
+
# Display the recovery context
|
|
596
|
+
cat "\$recovery_file" >&2
|
|
597
|
+
|
|
598
|
+
echo "" >&2
|
|
599
|
+
echo "═══════════════════════════════════════════════════════" >&2
|
|
600
|
+
echo "" >&2
|
|
601
|
+
|
|
602
|
+
# Remove after displaying (one-time recovery)
|
|
603
|
+
rm -f "\$recovery_file"
|
|
604
|
+
done
|
|
605
|
+
|
|
606
|
+
# Additional context if recovery was displayed
|
|
607
|
+
if [[ "\$FOUND_RECOVERY" == "true" ]]; then
|
|
608
|
+
echo "IMPORTANT: Your context was compacted. Review the recovery info above." >&2
|
|
609
|
+
echo "Recommended: Run 'pnpm wu:spawn --id \$WU_ID' for fresh full context." >&2
|
|
610
|
+
echo "" >&2
|
|
611
|
+
fi
|
|
612
|
+
|
|
613
|
+
exit 0
|
|
614
|
+
`;
|
|
615
|
+
/* eslint-enable no-useless-escape */
|
|
616
|
+
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import * as fs from 'node:fs';
|
|
11
11
|
import * as path from 'node:path';
|
|
12
12
|
import * as yaml from 'yaml';
|
|
13
|
-
import { generateEnforcementHooks, generateEnforceWorktreeScript, generateRequireWuScript, generateWarnIncompleteScript, } from './enforcement-generator.js';
|
|
13
|
+
import { generateEnforcementHooks, generateEnforceWorktreeScript, generateRequireWuScript, generateWarnIncompleteScript, generatePreCompactCheckpointScript, generateSessionStartRecoveryScript, HOOK_SCRIPTS, } from './enforcement-generator.js';
|
|
14
14
|
/**
|
|
15
15
|
* Read LumenFlow configuration from .lumenflow.config.yaml
|
|
16
16
|
*
|
|
@@ -161,6 +161,48 @@ function mergeHooksIntoSettings(existing, generated) {
|
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
163
|
}
|
|
164
|
+
// Merge PreCompact hooks (WU-1394)
|
|
165
|
+
if (generated.preCompact) {
|
|
166
|
+
if (!result.hooks.PreCompact) {
|
|
167
|
+
result.hooks.PreCompact = [];
|
|
168
|
+
}
|
|
169
|
+
for (const newHook of generated.preCompact) {
|
|
170
|
+
const existingIndex = result.hooks.PreCompact.findIndex((h) => h.matcher === newHook.matcher);
|
|
171
|
+
if (existingIndex >= 0) {
|
|
172
|
+
const existing = result.hooks.PreCompact[existingIndex];
|
|
173
|
+
for (const hook of newHook.hooks) {
|
|
174
|
+
const isDuplicate = existing.hooks.some((h) => h.command === hook.command);
|
|
175
|
+
if (!isDuplicate) {
|
|
176
|
+
existing.hooks.push(hook);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
result.hooks.PreCompact.push(newHook);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// Merge SessionStart hooks (WU-1394)
|
|
186
|
+
if (generated.sessionStart) {
|
|
187
|
+
if (!result.hooks.SessionStart) {
|
|
188
|
+
result.hooks.SessionStart = [];
|
|
189
|
+
}
|
|
190
|
+
for (const newHook of generated.sessionStart) {
|
|
191
|
+
const existingIndex = result.hooks.SessionStart.findIndex((h) => h.matcher === newHook.matcher);
|
|
192
|
+
if (existingIndex >= 0) {
|
|
193
|
+
const existing = result.hooks.SessionStart[existingIndex];
|
|
194
|
+
for (const hook of newHook.hooks) {
|
|
195
|
+
const isDuplicate = existing.hooks.some((h) => h.command === hook.command);
|
|
196
|
+
if (!isDuplicate) {
|
|
197
|
+
existing.hooks.push(hook);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
result.hooks.SessionStart.push(newHook);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
164
206
|
return result;
|
|
165
207
|
}
|
|
166
208
|
/**
|
|
@@ -191,14 +233,18 @@ export async function syncEnforcementHooks(projectDir) {
|
|
|
191
233
|
});
|
|
192
234
|
// Write hook scripts
|
|
193
235
|
if (enforcement.block_outside_worktree) {
|
|
194
|
-
writeHookScript(projectDir,
|
|
236
|
+
writeHookScript(projectDir, HOOK_SCRIPTS.ENFORCE_WORKTREE, generateEnforceWorktreeScript());
|
|
195
237
|
}
|
|
196
238
|
if (enforcement.require_wu_for_edits) {
|
|
197
|
-
writeHookScript(projectDir,
|
|
239
|
+
writeHookScript(projectDir, HOOK_SCRIPTS.REQUIRE_WU, generateRequireWuScript());
|
|
198
240
|
}
|
|
199
241
|
if (enforcement.warn_on_stop_without_wu_done) {
|
|
200
|
-
writeHookScript(projectDir,
|
|
242
|
+
writeHookScript(projectDir, HOOK_SCRIPTS.WARN_INCOMPLETE, generateWarnIncompleteScript());
|
|
201
243
|
}
|
|
244
|
+
// Always write recovery hook scripts when enforcement.hooks is enabled (WU-1394)
|
|
245
|
+
// These enable durable context recovery after compaction
|
|
246
|
+
writeHookScript(projectDir, HOOK_SCRIPTS.PRE_COMPACT_CHECKPOINT, generatePreCompactCheckpointScript());
|
|
247
|
+
writeHookScript(projectDir, HOOK_SCRIPTS.SESSION_START_RECOVERY, generateSessionStartRecoveryScript());
|
|
202
248
|
// Update settings.json
|
|
203
249
|
const existingSettings = readClaudeSettings(projectDir);
|
|
204
250
|
const updatedSettings = mergeHooksIntoSettings(existingSettings, generatedHooks);
|
|
@@ -215,8 +261,8 @@ export async function removeEnforcementHooks(projectDir) {
|
|
|
215
261
|
if (!settings.hooks) {
|
|
216
262
|
return;
|
|
217
263
|
}
|
|
218
|
-
// Remove enforcement-related hooks
|
|
219
|
-
const enforcementCommands =
|
|
264
|
+
// Remove enforcement-related hooks (includes all LumenFlow hook scripts)
|
|
265
|
+
const enforcementCommands = Object.values(HOOK_SCRIPTS);
|
|
220
266
|
if (settings.hooks.PreToolUse) {
|
|
221
267
|
settings.hooks.PreToolUse = settings.hooks.PreToolUse.map((entry) => ({
|
|
222
268
|
...entry,
|
package/dist/init.js
CHANGED
|
@@ -12,7 +12,7 @@ import * as path from 'node:path';
|
|
|
12
12
|
import * as yaml from 'yaml';
|
|
13
13
|
import { execFileSync } from 'node:child_process';
|
|
14
14
|
import { fileURLToPath } from 'node:url';
|
|
15
|
-
import { getDefaultConfig, createWUParser, WU_OPTIONS } from '@lumenflow/core';
|
|
15
|
+
import { getDefaultConfig, createWUParser, WU_OPTIONS, CLAUDE_HOOKS } from '@lumenflow/core';
|
|
16
16
|
// WU-1067: Import GATE_PRESETS for --preset support
|
|
17
17
|
import { GATE_PRESETS } from '@lumenflow/core/dist/gates-config.js';
|
|
18
18
|
// WU-1171: Import merge block utilities
|
|
@@ -699,6 +699,16 @@ pnpm wu:done --id WU-XXX
|
|
|
699
699
|
`;
|
|
700
700
|
// Template for .aider.conf.yml
|
|
701
701
|
const AIDER_CONF_TEMPLATE = `# Aider Configuration for LumenFlow Projects\n# See LUMENFLOW.md for workflow documentation\n\nmodel: gpt-4-turbo\nauto-commits: false\ndirty-commits: false\n\nread:\n - LUMENFLOW.md\n - .lumenflow/constraints.md\n`;
|
|
702
|
+
// WU-1413: Template for .mcp.json (MCP server configuration for Claude Code)
|
|
703
|
+
const MCP_JSON_TEMPLATE = `{
|
|
704
|
+
"mcpServers": {
|
|
705
|
+
"lumenflow": {
|
|
706
|
+
"command": "npx",
|
|
707
|
+
"args": ["@lumenflow/mcp"]
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
`;
|
|
702
712
|
// Template for docs/04-operations/tasks/backlog.md
|
|
703
713
|
const BACKLOG_TEMPLATE = `---\nsections:\n ready:\n heading: '## 🚀 Ready (pull from here)'\n insertion: after_heading_blank_line\n in_progress:\n heading: '## 🔧 In progress'\n insertion: after_heading_blank_line\n blocked:\n heading: '## ⛔ Blocked'\n insertion: after_heading_blank_line\n done:\n heading: '## ✅ Done'\n insertion: after_heading_blank_line\n---\n\n# Backlog (single source of truth)\n\n## 🚀 Ready (pull from here)\n\n(No items ready)\n\n## 🔧 In progress\n\n(No items in progress)\n\n## ⛔ Blocked\n\n(No items blocked)\n\n## ✅ Done\n\n(No items completed yet)\n`;
|
|
704
714
|
// Template for docs/04-operations/tasks/status.md
|
|
@@ -2364,6 +2374,9 @@ export async function scaffoldProject(targetDir, options) {
|
|
|
2364
2374
|
await createFile(path.join(targetDir, LUMENFLOW_AGENTS_DIR, '.gitkeep'), '', options.force ? 'force' : 'skip', result, targetDir);
|
|
2365
2375
|
// WU-1342: Create .gitignore with required exclusions
|
|
2366
2376
|
await scaffoldGitignore(targetDir, options, result);
|
|
2377
|
+
// WU-1408: Scaffold safe-git wrapper and pre-commit hook
|
|
2378
|
+
// These are core safety components needed for all projects
|
|
2379
|
+
await scaffoldSafetyScripts(targetDir, options, result);
|
|
2367
2380
|
// Optional: full docs scaffolding
|
|
2368
2381
|
if (options.full) {
|
|
2369
2382
|
await scaffoldFullDocs(targetDir, options, result, tokenDefaults);
|
|
@@ -2490,6 +2503,128 @@ const LUMENFLOW_SCRIPTS = {
|
|
|
2490
2503
|
gates: 'gates',
|
|
2491
2504
|
'gates:docs': 'gates --docs-only',
|
|
2492
2505
|
};
|
|
2506
|
+
/** WU-1408: Safety script path constants */
|
|
2507
|
+
const SCRIPTS_DIR = 'scripts';
|
|
2508
|
+
const SAFE_GIT_FILE = 'safe-git';
|
|
2509
|
+
const HUSKY_DIR = '.husky';
|
|
2510
|
+
const PRE_COMMIT_FILE = 'pre-commit';
|
|
2511
|
+
const SAFE_GIT_TEMPLATE_PATH = 'core/scripts/safe-git.template';
|
|
2512
|
+
const PRE_COMMIT_TEMPLATE_PATH = 'core/.husky/pre-commit.template';
|
|
2513
|
+
/**
|
|
2514
|
+
* WU-1408: Scaffold safety scripts (safe-git wrapper and pre-commit hook)
|
|
2515
|
+
* These are core safety components needed for LumenFlow enforcement:
|
|
2516
|
+
* - scripts/safe-git: Blocks dangerous git operations (e.g., manual worktree remove)
|
|
2517
|
+
* - .husky/pre-commit: Blocks direct commits to main/master, enforces WU workflow
|
|
2518
|
+
*
|
|
2519
|
+
* Both scripts are scaffolded in all modes (full and minimal) because they are
|
|
2520
|
+
* required for lumenflow-doctor to pass.
|
|
2521
|
+
*/
|
|
2522
|
+
async function scaffoldSafetyScripts(targetDir, options, result) {
|
|
2523
|
+
const fileMode = getFileMode(options);
|
|
2524
|
+
// Scaffold scripts/safe-git
|
|
2525
|
+
const safeGitPath = path.join(targetDir, SCRIPTS_DIR, SAFE_GIT_FILE);
|
|
2526
|
+
try {
|
|
2527
|
+
const safeGitTemplate = loadTemplate(SAFE_GIT_TEMPLATE_PATH);
|
|
2528
|
+
await createExecutableScript(safeGitPath, safeGitTemplate, fileMode, result, targetDir);
|
|
2529
|
+
}
|
|
2530
|
+
catch {
|
|
2531
|
+
// Fallback to hardcoded template if template file not found
|
|
2532
|
+
await createExecutableScript(safeGitPath, SAFE_GIT_TEMPLATE, fileMode, result, targetDir);
|
|
2533
|
+
}
|
|
2534
|
+
// Scaffold .husky/pre-commit
|
|
2535
|
+
const preCommitPath = path.join(targetDir, HUSKY_DIR, PRE_COMMIT_FILE);
|
|
2536
|
+
try {
|
|
2537
|
+
const preCommitTemplate = loadTemplate(PRE_COMMIT_TEMPLATE_PATH);
|
|
2538
|
+
await createExecutableScript(preCommitPath, preCommitTemplate, fileMode, result, targetDir);
|
|
2539
|
+
}
|
|
2540
|
+
catch {
|
|
2541
|
+
// Fallback to hardcoded template if template file not found
|
|
2542
|
+
await createExecutableScript(preCommitPath, PRE_COMMIT_TEMPLATE, fileMode, result, targetDir);
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
/**
|
|
2546
|
+
* WU-1408: Fallback safe-git template
|
|
2547
|
+
* Blocks dangerous git operations in LumenFlow environment
|
|
2548
|
+
*/
|
|
2549
|
+
const SAFE_GIT_TEMPLATE = `#!/bin/sh
|
|
2550
|
+
#
|
|
2551
|
+
# safe-git - LumenFlow safety wrapper for git
|
|
2552
|
+
#
|
|
2553
|
+
# Blocks dangerous operations that can corrupt agent state.
|
|
2554
|
+
# For all other commands, passes through to system git.
|
|
2555
|
+
#
|
|
2556
|
+
|
|
2557
|
+
set -e
|
|
2558
|
+
|
|
2559
|
+
# Block 'worktree remove'
|
|
2560
|
+
if [ "$1" = "worktree" ] && [ "$2" = "remove" ]; then
|
|
2561
|
+
echo "" >&2
|
|
2562
|
+
echo "=== LUMENFLOW SAFETY BLOCK ===" >&2
|
|
2563
|
+
echo "" >&2
|
|
2564
|
+
echo "BLOCKED: Manual 'git worktree remove' is unsafe in this environment." >&2
|
|
2565
|
+
echo "" >&2
|
|
2566
|
+
echo "REASON: Manual removal leaves orphan directories and corrupts agent state." >&2
|
|
2567
|
+
echo "" >&2
|
|
2568
|
+
echo "USE INSTEAD:" >&2
|
|
2569
|
+
echo " pnpm wu:done --id <ID> (To complete a task)" >&2
|
|
2570
|
+
echo " pnpm wu:cleanup --id <ID> (To discard a task)" >&2
|
|
2571
|
+
echo "==============================" >&2
|
|
2572
|
+
exit 1
|
|
2573
|
+
fi
|
|
2574
|
+
|
|
2575
|
+
# Pass through to real git
|
|
2576
|
+
exec git "$@"
|
|
2577
|
+
`;
|
|
2578
|
+
/**
|
|
2579
|
+
* WU-1408: Fallback pre-commit template
|
|
2580
|
+
* Blocks direct commits to main/master, allows commits on lane branches
|
|
2581
|
+
* Does NOT run pnpm test (which fails on new projects)
|
|
2582
|
+
*/
|
|
2583
|
+
const PRE_COMMIT_TEMPLATE = `#!/bin/sh
|
|
2584
|
+
#
|
|
2585
|
+
# LumenFlow Pre-Commit Hook
|
|
2586
|
+
#
|
|
2587
|
+
# Enforces worktree discipline by blocking direct commits to main/master.
|
|
2588
|
+
# Does NOT assume pnpm test or any other commands exist.
|
|
2589
|
+
#
|
|
2590
|
+
# Rules:
|
|
2591
|
+
# 1. BLOCK commits to main/master (use WU workflow instead)
|
|
2592
|
+
# 2. ALLOW commits on lane branches (lane/*/wu-*)
|
|
2593
|
+
# 3. ALLOW commits on tmp/* branches (CLI micro-worktrees)
|
|
2594
|
+
#
|
|
2595
|
+
|
|
2596
|
+
# Skip on tmp/* branches (CLI micro-worktrees)
|
|
2597
|
+
BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
|
|
2598
|
+
case "$BRANCH" in tmp/*) exit 0 ;; esac
|
|
2599
|
+
|
|
2600
|
+
# Check for force bypass
|
|
2601
|
+
if [ "$LUMENFLOW_FORCE" = "1" ]; then
|
|
2602
|
+
exit 0
|
|
2603
|
+
fi
|
|
2604
|
+
|
|
2605
|
+
# Block direct commits to main/master
|
|
2606
|
+
case "$BRANCH" in
|
|
2607
|
+
main|master)
|
|
2608
|
+
echo "" >&2
|
|
2609
|
+
echo "=== DIRECT COMMIT TO \${BRANCH} BLOCKED ===" >&2
|
|
2610
|
+
echo "" >&2
|
|
2611
|
+
echo "LumenFlow protects main from direct commits." >&2
|
|
2612
|
+
echo "" >&2
|
|
2613
|
+
echo "USE INSTEAD:" >&2
|
|
2614
|
+
echo " pnpm wu:claim --id WU-XXXX --lane \\"<Lane>\\"" >&2
|
|
2615
|
+
echo " cd worktrees/<lane>-wu-xxxx" >&2
|
|
2616
|
+
echo " # Make commits in the worktree" >&2
|
|
2617
|
+
echo "" >&2
|
|
2618
|
+
echo "EMERGENCY BYPASS (logged):" >&2
|
|
2619
|
+
echo " LUMENFLOW_FORCE=1 git commit ..." >&2
|
|
2620
|
+
echo "==========================================" >&2
|
|
2621
|
+
exit 1
|
|
2622
|
+
;;
|
|
2623
|
+
esac
|
|
2624
|
+
|
|
2625
|
+
# Allow commits on other branches
|
|
2626
|
+
exit 0
|
|
2627
|
+
`;
|
|
2493
2628
|
/**
|
|
2494
2629
|
* WU-1300: Inject LumenFlow scripts into package.json
|
|
2495
2630
|
* - Creates package.json if it doesn't exist
|
|
@@ -2648,7 +2783,43 @@ async function scaffoldClientFiles(targetDir, options, result, tokens, client) {
|
|
|
2648
2783
|
await createFile(path.join(targetDir, 'CLAUDE.md'), processTemplate(CLAUDE_MD_TEMPLATE, tokens), fileMode, result, targetDir);
|
|
2649
2784
|
await createDirectory(path.join(targetDir, CLAUDE_AGENTS_DIR), result, targetDir);
|
|
2650
2785
|
await createFile(path.join(targetDir, CLAUDE_AGENTS_DIR, '.gitkeep'), '', options.force ? 'force' : 'skip', result, targetDir);
|
|
2651
|
-
|
|
2786
|
+
// WU-1394: Load settings.json from template (includes PreCompact/SessionStart hooks)
|
|
2787
|
+
let settingsContent;
|
|
2788
|
+
try {
|
|
2789
|
+
settingsContent = loadTemplate(CLAUDE_HOOKS.TEMPLATES.SETTINGS);
|
|
2790
|
+
}
|
|
2791
|
+
catch {
|
|
2792
|
+
settingsContent = CLAUDE_SETTINGS_TEMPLATE;
|
|
2793
|
+
}
|
|
2794
|
+
await createFile(path.join(targetDir, CLAUDE_DIR, 'settings.json'), settingsContent, options.force ? 'force' : 'skip', result, targetDir);
|
|
2795
|
+
// WU-1413: Scaffold .mcp.json for MCP server integration
|
|
2796
|
+
let mcpJsonContent;
|
|
2797
|
+
try {
|
|
2798
|
+
mcpJsonContent = loadTemplate('core/.mcp.json.template');
|
|
2799
|
+
}
|
|
2800
|
+
catch {
|
|
2801
|
+
mcpJsonContent = MCP_JSON_TEMPLATE;
|
|
2802
|
+
}
|
|
2803
|
+
await createFile(path.join(targetDir, '.mcp.json'), mcpJsonContent, fileMode, result, targetDir);
|
|
2804
|
+
// WU-1394: Scaffold recovery hook scripts with executable permissions
|
|
2805
|
+
const hooksDir = path.join(targetDir, CLAUDE_DIR, 'hooks');
|
|
2806
|
+
await createDirectory(hooksDir, result, targetDir);
|
|
2807
|
+
// Load and write pre-compact-checkpoint.sh
|
|
2808
|
+
try {
|
|
2809
|
+
const preCompactScript = loadTemplate(CLAUDE_HOOKS.TEMPLATES.PRE_COMPACT);
|
|
2810
|
+
await createExecutableScript(path.join(hooksDir, CLAUDE_HOOKS.SCRIPTS.PRE_COMPACT_CHECKPOINT), preCompactScript, options.force ? 'force' : 'skip', result, targetDir);
|
|
2811
|
+
}
|
|
2812
|
+
catch {
|
|
2813
|
+
// Template not found - hook won't be scaffolded
|
|
2814
|
+
}
|
|
2815
|
+
// Load and write session-start-recovery.sh
|
|
2816
|
+
try {
|
|
2817
|
+
const sessionStartScript = loadTemplate(CLAUDE_HOOKS.TEMPLATES.SESSION_START);
|
|
2818
|
+
await createExecutableScript(path.join(hooksDir, CLAUDE_HOOKS.SCRIPTS.SESSION_START_RECOVERY), sessionStartScript, options.force ? 'force' : 'skip', result, targetDir);
|
|
2819
|
+
}
|
|
2820
|
+
catch {
|
|
2821
|
+
// Template not found - hook won't be scaffolded
|
|
2822
|
+
}
|
|
2652
2823
|
// WU-1083: Scaffold Claude skills
|
|
2653
2824
|
await scaffoldClaudeSkills(targetDir, options, result, tokens);
|
|
2654
2825
|
// WU-1083: Scaffold agent onboarding docs for Claude vendor (even without --full)
|
|
@@ -2774,6 +2945,28 @@ function writeNewFile(filePath, content, result, relativePath) {
|
|
|
2774
2945
|
fs.writeFileSync(filePath, content);
|
|
2775
2946
|
result.created.push(relativePath);
|
|
2776
2947
|
}
|
|
2948
|
+
/**
|
|
2949
|
+
* WU-1394: Create an executable script file with proper permissions
|
|
2950
|
+
* Similar to createFile but sets 0o755 mode for shell scripts
|
|
2951
|
+
*/
|
|
2952
|
+
async function createExecutableScript(filePath, content, mode, result, targetDir) {
|
|
2953
|
+
const relativePath = getRelativePath(targetDir, filePath);
|
|
2954
|
+
const resolvedMode = resolveBooleanToFileMode(mode);
|
|
2955
|
+
result.merged = result.merged ?? [];
|
|
2956
|
+
result.warnings = result.warnings ?? [];
|
|
2957
|
+
const fileExists = fs.existsSync(filePath);
|
|
2958
|
+
if (fileExists && resolvedMode === 'skip') {
|
|
2959
|
+
result.skipped.push(relativePath);
|
|
2960
|
+
return;
|
|
2961
|
+
}
|
|
2962
|
+
// Write file with executable permissions
|
|
2963
|
+
const parentDir = path.dirname(filePath);
|
|
2964
|
+
if (!fs.existsSync(parentDir)) {
|
|
2965
|
+
fs.mkdirSync(parentDir, { recursive: true });
|
|
2966
|
+
}
|
|
2967
|
+
fs.writeFileSync(filePath, content, { mode: 0o755 });
|
|
2968
|
+
result.created.push(relativePath);
|
|
2969
|
+
}
|
|
2777
2970
|
/**
|
|
2778
2971
|
* CLI entry point
|
|
2779
2972
|
* WU-1085: Updated to use parseInitOptions for proper --help support
|