@lumenflow/cli 2.9.0 → 2.10.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 +1 -1
- package/dist/hooks/enforcement-generator.js +256 -5
- package/dist/hooks/enforcement-sync.js +52 -6
- package/dist/init.js +51 -2
- package/dist/mem-recover.js +221 -0
- package/package.json +6 -6
- 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/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
package/README.md
CHANGED
|
@@ -134,7 +134,7 @@ This package provides CLI commands for the LumenFlow workflow framework, includi
|
|
|
134
134
|
| Command | Description |
|
|
135
135
|
| ----------------- | ------------------------------------------------------------------------------------------ |
|
|
136
136
|
| `gates` | Run quality gates with support for docs-only mode, incremental linting, and tiered testing |
|
|
137
|
-
| `lumenflow-gates` | Alias for `gates` - run quality gates
|
|
137
|
+
| `lumenflow-gates` | Alias for `gates` - run quality gates with support for docs-only mode and tiered testing |
|
|
138
138
|
|
|
139
139
|
### System & Setup
|
|
140
140
|
|
|
@@ -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
|
|
@@ -2648,7 +2648,34 @@ async function scaffoldClientFiles(targetDir, options, result, tokens, client) {
|
|
|
2648
2648
|
await createFile(path.join(targetDir, 'CLAUDE.md'), processTemplate(CLAUDE_MD_TEMPLATE, tokens), fileMode, result, targetDir);
|
|
2649
2649
|
await createDirectory(path.join(targetDir, CLAUDE_AGENTS_DIR), result, targetDir);
|
|
2650
2650
|
await createFile(path.join(targetDir, CLAUDE_AGENTS_DIR, '.gitkeep'), '', options.force ? 'force' : 'skip', result, targetDir);
|
|
2651
|
-
|
|
2651
|
+
// WU-1394: Load settings.json from template (includes PreCompact/SessionStart hooks)
|
|
2652
|
+
let settingsContent;
|
|
2653
|
+
try {
|
|
2654
|
+
settingsContent = loadTemplate(CLAUDE_HOOKS.TEMPLATES.SETTINGS);
|
|
2655
|
+
}
|
|
2656
|
+
catch {
|
|
2657
|
+
settingsContent = CLAUDE_SETTINGS_TEMPLATE;
|
|
2658
|
+
}
|
|
2659
|
+
await createFile(path.join(targetDir, CLAUDE_DIR, 'settings.json'), settingsContent, options.force ? 'force' : 'skip', result, targetDir);
|
|
2660
|
+
// WU-1394: Scaffold recovery hook scripts with executable permissions
|
|
2661
|
+
const hooksDir = path.join(targetDir, CLAUDE_DIR, 'hooks');
|
|
2662
|
+
await createDirectory(hooksDir, result, targetDir);
|
|
2663
|
+
// Load and write pre-compact-checkpoint.sh
|
|
2664
|
+
try {
|
|
2665
|
+
const preCompactScript = loadTemplate(CLAUDE_HOOKS.TEMPLATES.PRE_COMPACT);
|
|
2666
|
+
await createExecutableScript(path.join(hooksDir, CLAUDE_HOOKS.SCRIPTS.PRE_COMPACT_CHECKPOINT), preCompactScript, options.force ? 'force' : 'skip', result, targetDir);
|
|
2667
|
+
}
|
|
2668
|
+
catch {
|
|
2669
|
+
// Template not found - hook won't be scaffolded
|
|
2670
|
+
}
|
|
2671
|
+
// Load and write session-start-recovery.sh
|
|
2672
|
+
try {
|
|
2673
|
+
const sessionStartScript = loadTemplate(CLAUDE_HOOKS.TEMPLATES.SESSION_START);
|
|
2674
|
+
await createExecutableScript(path.join(hooksDir, CLAUDE_HOOKS.SCRIPTS.SESSION_START_RECOVERY), sessionStartScript, options.force ? 'force' : 'skip', result, targetDir);
|
|
2675
|
+
}
|
|
2676
|
+
catch {
|
|
2677
|
+
// Template not found - hook won't be scaffolded
|
|
2678
|
+
}
|
|
2652
2679
|
// WU-1083: Scaffold Claude skills
|
|
2653
2680
|
await scaffoldClaudeSkills(targetDir, options, result, tokens);
|
|
2654
2681
|
// WU-1083: Scaffold agent onboarding docs for Claude vendor (even without --full)
|
|
@@ -2774,6 +2801,28 @@ function writeNewFile(filePath, content, result, relativePath) {
|
|
|
2774
2801
|
fs.writeFileSync(filePath, content);
|
|
2775
2802
|
result.created.push(relativePath);
|
|
2776
2803
|
}
|
|
2804
|
+
/**
|
|
2805
|
+
* WU-1394: Create an executable script file with proper permissions
|
|
2806
|
+
* Similar to createFile but sets 0o755 mode for shell scripts
|
|
2807
|
+
*/
|
|
2808
|
+
async function createExecutableScript(filePath, content, mode, result, targetDir) {
|
|
2809
|
+
const relativePath = getRelativePath(targetDir, filePath);
|
|
2810
|
+
const resolvedMode = resolveBooleanToFileMode(mode);
|
|
2811
|
+
result.merged = result.merged ?? [];
|
|
2812
|
+
result.warnings = result.warnings ?? [];
|
|
2813
|
+
const fileExists = fs.existsSync(filePath);
|
|
2814
|
+
if (fileExists && resolvedMode === 'skip') {
|
|
2815
|
+
result.skipped.push(relativePath);
|
|
2816
|
+
return;
|
|
2817
|
+
}
|
|
2818
|
+
// Write file with executable permissions
|
|
2819
|
+
const parentDir = path.dirname(filePath);
|
|
2820
|
+
if (!fs.existsSync(parentDir)) {
|
|
2821
|
+
fs.mkdirSync(parentDir, { recursive: true });
|
|
2822
|
+
}
|
|
2823
|
+
fs.writeFileSync(filePath, content, { mode: 0o755 });
|
|
2824
|
+
result.created.push(relativePath);
|
|
2825
|
+
}
|
|
2777
2826
|
/**
|
|
2778
2827
|
* CLI entry point
|
|
2779
2828
|
* WU-1085: Updated to use parseInitOptions for proper --help support
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Memory Recover CLI (WU-1390)
|
|
4
|
+
*
|
|
5
|
+
* Generate post-compaction recovery context for agents that have lost
|
|
6
|
+
* their LumenFlow instructions due to context compaction.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* pnpm mem:recover --wu WU-XXXX [options]
|
|
10
|
+
*
|
|
11
|
+
* Options:
|
|
12
|
+
* --max-size <bytes> Maximum output size in bytes (default: 2048)
|
|
13
|
+
* --format <json|human> Output format (default: human)
|
|
14
|
+
* --quiet Suppress header/footer output
|
|
15
|
+
*
|
|
16
|
+
* The recovery context includes:
|
|
17
|
+
* - Last checkpoint for the WU
|
|
18
|
+
* - Compact constraints (7 rules)
|
|
19
|
+
* - Essential CLI commands
|
|
20
|
+
* - Guidance to spawn fresh agent
|
|
21
|
+
*
|
|
22
|
+
* @see {@link packages/@lumenflow/memory/src/mem-recover-core.ts} - Core logic
|
|
23
|
+
* @see {@link packages/@lumenflow/memory/__tests__/mem-recover-core.test.ts} - Tests
|
|
24
|
+
*/
|
|
25
|
+
import fs from 'node:fs/promises';
|
|
26
|
+
import path from 'node:path';
|
|
27
|
+
import { generateRecoveryContext } from '@lumenflow/memory/dist/mem-recover-core.js';
|
|
28
|
+
import { createWUParser, WU_OPTIONS } from '@lumenflow/core/dist/arg-parser.js';
|
|
29
|
+
import { EXIT_CODES, LUMENFLOW_PATHS } from '@lumenflow/core/dist/wu-constants.js';
|
|
30
|
+
/**
|
|
31
|
+
* Log prefix for mem:recover output
|
|
32
|
+
*/
|
|
33
|
+
const LOG_PREFIX = '[mem:recover]';
|
|
34
|
+
/**
|
|
35
|
+
* Tool name for audit logging
|
|
36
|
+
*/
|
|
37
|
+
const TOOL_NAME = 'mem:recover';
|
|
38
|
+
/**
|
|
39
|
+
* Valid output formats
|
|
40
|
+
*/
|
|
41
|
+
const VALID_FORMATS = ['json', 'human'];
|
|
42
|
+
/**
|
|
43
|
+
* CLI argument options specific to mem:recover
|
|
44
|
+
*/
|
|
45
|
+
const CLI_OPTIONS = {
|
|
46
|
+
maxSize: {
|
|
47
|
+
name: 'maxSize',
|
|
48
|
+
flags: '-m, --max-size <bytes>',
|
|
49
|
+
description: 'Maximum output size in bytes (default: 2048)',
|
|
50
|
+
},
|
|
51
|
+
format: {
|
|
52
|
+
name: 'format',
|
|
53
|
+
flags: '-f, --format <format>',
|
|
54
|
+
description: 'Output format: json or human (default: human)',
|
|
55
|
+
},
|
|
56
|
+
baseDir: {
|
|
57
|
+
name: 'baseDir',
|
|
58
|
+
flags: '-d, --base-dir <path>',
|
|
59
|
+
description: 'Base directory (defaults to current directory)',
|
|
60
|
+
},
|
|
61
|
+
quiet: {
|
|
62
|
+
name: 'quiet',
|
|
63
|
+
flags: '-q, --quiet',
|
|
64
|
+
description: 'Suppress header/footer output, only show recovery context',
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Write audit log entry for tool execution
|
|
69
|
+
*/
|
|
70
|
+
async function writeAuditLog(baseDir, entry) {
|
|
71
|
+
try {
|
|
72
|
+
const logPath = path.join(baseDir, LUMENFLOW_PATHS.AUDIT_LOG);
|
|
73
|
+
const logDir = path.dirname(logPath);
|
|
74
|
+
await fs.mkdir(logDir, { recursive: true });
|
|
75
|
+
const line = `${JSON.stringify(entry)}\n`;
|
|
76
|
+
await fs.appendFile(logPath, line, 'utf-8');
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// Audit logging is non-fatal - silently ignore errors
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Validate and parse a positive integer argument
|
|
84
|
+
*/
|
|
85
|
+
function parsePositiveInt(value, optionName) {
|
|
86
|
+
if (!value) {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
const parsed = parseInt(value, 10);
|
|
90
|
+
if (isNaN(parsed) || parsed <= 0) {
|
|
91
|
+
throw new Error(`Invalid ${optionName} value: "${value}". Must be a positive integer.`);
|
|
92
|
+
}
|
|
93
|
+
return parsed;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Validate format argument
|
|
97
|
+
*/
|
|
98
|
+
function validateFormat(format) {
|
|
99
|
+
if (!format) {
|
|
100
|
+
return 'human';
|
|
101
|
+
}
|
|
102
|
+
if (!VALID_FORMATS.includes(format)) {
|
|
103
|
+
throw new Error(`Invalid --format value: "${format}". Valid formats: ${VALID_FORMATS.join(', ')}`);
|
|
104
|
+
}
|
|
105
|
+
return format;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Print output in human-readable format
|
|
109
|
+
*/
|
|
110
|
+
function printHumanFormat(result, wuId, quiet) {
|
|
111
|
+
if (!quiet) {
|
|
112
|
+
console.log(`${LOG_PREFIX} Recovery context for ${wuId}:`);
|
|
113
|
+
console.log('');
|
|
114
|
+
}
|
|
115
|
+
console.log(result.context);
|
|
116
|
+
if (!quiet) {
|
|
117
|
+
console.log('');
|
|
118
|
+
console.log(`${LOG_PREFIX} ${result.size} bytes${result.truncated ? ' (truncated)' : ''}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Print output in JSON format
|
|
123
|
+
*/
|
|
124
|
+
function printJsonFormat(result, wuId) {
|
|
125
|
+
const output = {
|
|
126
|
+
wuId,
|
|
127
|
+
context: result.context,
|
|
128
|
+
size: result.size,
|
|
129
|
+
truncated: result.truncated,
|
|
130
|
+
};
|
|
131
|
+
console.log(JSON.stringify(output, null, 2));
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Main CLI entry point
|
|
135
|
+
*/
|
|
136
|
+
async function main() {
|
|
137
|
+
const args = createWUParser({
|
|
138
|
+
name: 'mem-recover',
|
|
139
|
+
description: 'Generate post-compaction recovery context for agents',
|
|
140
|
+
options: [
|
|
141
|
+
WU_OPTIONS.wu,
|
|
142
|
+
CLI_OPTIONS.maxSize,
|
|
143
|
+
CLI_OPTIONS.format,
|
|
144
|
+
CLI_OPTIONS.baseDir,
|
|
145
|
+
CLI_OPTIONS.quiet,
|
|
146
|
+
],
|
|
147
|
+
required: ['wu'],
|
|
148
|
+
});
|
|
149
|
+
const baseDir = args.baseDir || process.cwd();
|
|
150
|
+
const startedAt = new Date().toISOString();
|
|
151
|
+
const startTime = Date.now();
|
|
152
|
+
let maxSize;
|
|
153
|
+
let format;
|
|
154
|
+
// Validate arguments
|
|
155
|
+
try {
|
|
156
|
+
maxSize = parsePositiveInt(args.maxSize, '--max-size');
|
|
157
|
+
format = validateFormat(args.format);
|
|
158
|
+
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
const error = err;
|
|
161
|
+
console.error(`${LOG_PREFIX} Error: ${error.message}`);
|
|
162
|
+
process.exit(EXIT_CODES.ERROR);
|
|
163
|
+
}
|
|
164
|
+
let result;
|
|
165
|
+
let error = null;
|
|
166
|
+
try {
|
|
167
|
+
result = await generateRecoveryContext({
|
|
168
|
+
wuId: args.wu,
|
|
169
|
+
baseDir,
|
|
170
|
+
maxSize,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
catch (err) {
|
|
174
|
+
const e = err;
|
|
175
|
+
error = e.message;
|
|
176
|
+
result = {
|
|
177
|
+
success: false,
|
|
178
|
+
context: '',
|
|
179
|
+
size: 0,
|
|
180
|
+
truncated: false,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
const durationMs = Date.now() - startTime;
|
|
184
|
+
// Write audit log entry
|
|
185
|
+
await writeAuditLog(baseDir, {
|
|
186
|
+
tool: TOOL_NAME,
|
|
187
|
+
status: error ? 'failed' : 'success',
|
|
188
|
+
startedAt,
|
|
189
|
+
completedAt: new Date().toISOString(),
|
|
190
|
+
durationMs,
|
|
191
|
+
input: {
|
|
192
|
+
baseDir,
|
|
193
|
+
wuId: args.wu,
|
|
194
|
+
maxSize,
|
|
195
|
+
format,
|
|
196
|
+
quiet: args.quiet,
|
|
197
|
+
},
|
|
198
|
+
output: result.success
|
|
199
|
+
? {
|
|
200
|
+
contextSize: result.size,
|
|
201
|
+
truncated: result.truncated,
|
|
202
|
+
}
|
|
203
|
+
: null,
|
|
204
|
+
error: error ? { message: error } : null,
|
|
205
|
+
});
|
|
206
|
+
if (error) {
|
|
207
|
+
console.error(`${LOG_PREFIX} Error: ${error}`);
|
|
208
|
+
process.exit(EXIT_CODES.ERROR);
|
|
209
|
+
}
|
|
210
|
+
// Print output based on format
|
|
211
|
+
if (format === 'json') {
|
|
212
|
+
printJsonFormat(result, args.wu);
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
printHumanFormat(result, args.wu, !!args.quiet);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
main().catch((e) => {
|
|
219
|
+
console.error(`${LOG_PREFIX} ${e.message}`);
|
|
220
|
+
process.exit(EXIT_CODES.ERROR);
|
|
221
|
+
});
|