@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.
Files changed (31) hide show
  1. package/README.md +23 -2
  2. package/dist/__tests__/gates-integration-tests.test.js +112 -0
  3. package/dist/__tests__/init.test.js +225 -0
  4. package/dist/__tests__/safe-git.test.js +4 -4
  5. package/dist/__tests__/wu-create-required-fields.test.js +22 -0
  6. package/dist/__tests__/wu-create.test.js +72 -0
  7. package/dist/gates.js +6 -8
  8. package/dist/hooks/enforcement-generator.js +256 -5
  9. package/dist/hooks/enforcement-sync.js +52 -6
  10. package/dist/init.js +195 -2
  11. package/dist/mem-recover.js +221 -0
  12. package/dist/orchestrate-initiative.js +19 -1
  13. package/dist/state-doctor-fix.js +36 -1
  14. package/dist/state-doctor.js +10 -6
  15. package/dist/wu-create.js +37 -15
  16. package/dist/wu-recover.js +53 -2
  17. package/dist/wu-spawn.js +2 -2
  18. package/package.json +6 -6
  19. package/templates/core/.mcp.json.template +8 -0
  20. package/templates/core/LUMENFLOW.md.template +24 -0
  21. package/templates/core/ai/onboarding/first-wu-mistakes.md.template +47 -0
  22. package/templates/core/ai/onboarding/lumenflow-force-usage.md.template +183 -0
  23. package/templates/core/ai/onboarding/quick-ref-commands.md.template +68 -55
  24. package/templates/core/ai/onboarding/release-process.md.template +58 -4
  25. package/templates/core/ai/onboarding/starting-prompt.md.template +67 -3
  26. package/templates/core/ai/onboarding/vendor-support.md.template +73 -0
  27. package/templates/core/scripts/safe-git.template +29 -0
  28. package/templates/vendors/claude/.claude/hooks/pre-compact-checkpoint.sh +102 -0
  29. package/templates/vendors/claude/.claude/hooks/session-start-recovery.sh +74 -0
  30. package/templates/vendors/claude/.claude/settings.json.template +42 -0
  31. 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: '$CLAUDE_PROJECT_DIR/.claude/hooks/enforce-worktree.sh',
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: '$CLAUDE_PROJECT_DIR/.claude/hooks/require-wu.sh',
32
+ command: getHookCommand(HOOK_SCRIPTS.REQUIRE_WU),
30
33
  });
31
34
  }
32
35
  if (writeEditHooks.length > 0) {
33
36
  preToolUseHooks.push({
34
- matcher: 'Write|Edit',
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: '$CLAUDE_PROJECT_DIR/.claude/hooks/warn-incomplete.sh',
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, 'enforce-worktree.sh', generateEnforceWorktreeScript());
236
+ writeHookScript(projectDir, HOOK_SCRIPTS.ENFORCE_WORKTREE, generateEnforceWorktreeScript());
195
237
  }
196
238
  if (enforcement.require_wu_for_edits) {
197
- writeHookScript(projectDir, 'require-wu.sh', generateRequireWuScript());
239
+ writeHookScript(projectDir, HOOK_SCRIPTS.REQUIRE_WU, generateRequireWuScript());
198
240
  }
199
241
  if (enforcement.warn_on_stop_without_wu_done) {
200
- writeHookScript(projectDir, 'warn-incomplete.sh', generateWarnIncompleteScript());
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 = ['enforce-worktree.sh', 'require-wu.sh', 'warn-incomplete.sh'];
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
- await createFile(path.join(targetDir, CLAUDE_DIR, 'settings.json'), CLAUDE_SETTINGS_TEMPLATE, options.force ? 'force' : 'skip', result, targetDir);
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