@lumenflow/cli 2.18.2 → 2.19.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 (104) hide show
  1. package/README.md +42 -41
  2. package/dist/delegation-list.js +140 -0
  3. package/dist/delegation-list.js.map +1 -0
  4. package/dist/doctor.js +35 -99
  5. package/dist/doctor.js.map +1 -1
  6. package/dist/gates-plan-resolvers.js +150 -0
  7. package/dist/gates-plan-resolvers.js.map +1 -0
  8. package/dist/gates-runners.js +533 -0
  9. package/dist/gates-runners.js.map +1 -0
  10. package/dist/gates-types.js +3 -0
  11. package/dist/gates-types.js.map +1 -1
  12. package/dist/gates-utils.js +316 -0
  13. package/dist/gates-utils.js.map +1 -0
  14. package/dist/gates.js +44 -1016
  15. package/dist/gates.js.map +1 -1
  16. package/dist/hooks/enforcement-generator.js +16 -880
  17. package/dist/hooks/enforcement-generator.js.map +1 -1
  18. package/dist/hooks/enforcement-sync.js +1 -4
  19. package/dist/hooks/enforcement-sync.js.map +1 -1
  20. package/dist/hooks/generators/auto-checkpoint.js +123 -0
  21. package/dist/hooks/generators/auto-checkpoint.js.map +1 -0
  22. package/dist/hooks/generators/enforce-worktree.js +188 -0
  23. package/dist/hooks/generators/enforce-worktree.js.map +1 -0
  24. package/dist/hooks/generators/index.js +16 -0
  25. package/dist/hooks/generators/index.js.map +1 -0
  26. package/dist/hooks/generators/pre-compact-checkpoint.js +134 -0
  27. package/dist/hooks/generators/pre-compact-checkpoint.js.map +1 -0
  28. package/dist/hooks/generators/require-wu.js +115 -0
  29. package/dist/hooks/generators/require-wu.js.map +1 -0
  30. package/dist/hooks/generators/session-start-recovery.js +101 -0
  31. package/dist/hooks/generators/session-start-recovery.js.map +1 -0
  32. package/dist/hooks/generators/signal-utils.js +52 -0
  33. package/dist/hooks/generators/signal-utils.js.map +1 -0
  34. package/dist/hooks/generators/warn-incomplete.js +65 -0
  35. package/dist/hooks/generators/warn-incomplete.js.map +1 -0
  36. package/dist/init-detection.js +228 -0
  37. package/dist/init-detection.js.map +1 -0
  38. package/dist/init-scaffolding.js +146 -0
  39. package/dist/init-scaffolding.js.map +1 -0
  40. package/dist/init-templates.js +1928 -0
  41. package/dist/init-templates.js.map +1 -0
  42. package/dist/init.js +136 -2425
  43. package/dist/init.js.map +1 -1
  44. package/dist/initiative-edit.js +42 -11
  45. package/dist/initiative-edit.js.map +1 -1
  46. package/dist/initiative-remove-wu.js +0 -0
  47. package/dist/initiative-status.js +29 -2
  48. package/dist/initiative-status.js.map +1 -1
  49. package/dist/mem-context.js +22 -9
  50. package/dist/mem-context.js.map +1 -1
  51. package/dist/orchestrate-init-status.js +32 -1
  52. package/dist/orchestrate-init-status.js.map +1 -1
  53. package/dist/orchestrate-monitor.js +38 -38
  54. package/dist/orchestrate-monitor.js.map +1 -1
  55. package/dist/public-manifest.js +12 -5
  56. package/dist/public-manifest.js.map +1 -1
  57. package/dist/shared-validators.js +1 -0
  58. package/dist/shared-validators.js.map +1 -1
  59. package/dist/spawn-list.js +0 -0
  60. package/dist/wu-claim-branch.js +121 -0
  61. package/dist/wu-claim-branch.js.map +1 -0
  62. package/dist/wu-claim-output.js +83 -0
  63. package/dist/wu-claim-output.js.map +1 -0
  64. package/dist/wu-claim-resume-handler.js +85 -0
  65. package/dist/wu-claim-resume-handler.js.map +1 -0
  66. package/dist/wu-claim-state.js +572 -0
  67. package/dist/wu-claim-state.js.map +1 -0
  68. package/dist/wu-claim-validation.js +439 -0
  69. package/dist/wu-claim-validation.js.map +1 -0
  70. package/dist/wu-claim-worktree.js +221 -0
  71. package/dist/wu-claim-worktree.js.map +1 -0
  72. package/dist/wu-claim.js +54 -1402
  73. package/dist/wu-claim.js.map +1 -1
  74. package/dist/wu-create-content.js +254 -0
  75. package/dist/wu-create-content.js.map +1 -0
  76. package/dist/wu-create-readiness.js +57 -0
  77. package/dist/wu-create-readiness.js.map +1 -0
  78. package/dist/wu-create-validation.js +149 -0
  79. package/dist/wu-create-validation.js.map +1 -0
  80. package/dist/wu-create.js +39 -441
  81. package/dist/wu-create.js.map +1 -1
  82. package/dist/wu-done.js +144 -249
  83. package/dist/wu-done.js.map +1 -1
  84. package/dist/wu-edit-operations.js +432 -0
  85. package/dist/wu-edit-operations.js.map +1 -0
  86. package/dist/wu-edit-validators.js +280 -0
  87. package/dist/wu-edit-validators.js.map +1 -0
  88. package/dist/wu-edit.js +27 -713
  89. package/dist/wu-edit.js.map +1 -1
  90. package/dist/wu-prep.js +32 -2
  91. package/dist/wu-prep.js.map +1 -1
  92. package/dist/wu-repair.js +1 -1
  93. package/dist/wu-repair.js.map +1 -1
  94. package/dist/wu-spawn-prompt-builders.js +1123 -0
  95. package/dist/wu-spawn-prompt-builders.js.map +1 -0
  96. package/dist/wu-spawn-strategy-resolver.js +314 -0
  97. package/dist/wu-spawn-strategy-resolver.js.map +1 -0
  98. package/dist/wu-spawn.js +9 -1398
  99. package/dist/wu-spawn.js.map +1 -1
  100. package/package.json +10 -7
  101. package/templates/core/LUMENFLOW.md.template +29 -99
  102. package/templates/core/ai/onboarding/agent-invocation-guide.md.template +1 -1
  103. package/templates/core/ai/onboarding/quick-ref-commands.md.template +29 -4
  104. package/templates/vendors/claude/.claude/skills/orchestration/SKILL.md.template +8 -8
@@ -2,11 +2,12 @@
2
2
  * @file enforcement-generator.ts
3
3
  * Generates Claude Code enforcement hooks based on configuration (WU-1367)
4
4
  *
5
- * This module generates hook configurations that can be written to
6
- * .claude/settings.json to enforce LumenFlow workflow compliance.
5
+ * This module is the dispatcher/orchestrator entrypoint for hook generation.
6
+ * Individual hook script builders live under ./generators/ (WU-1645).
7
+ *
8
+ * All public exports are preserved for backward compatibility.
7
9
  */
8
10
  import { CLAUDE_HOOKS, getHookCommand } from '@lumenflow/core';
9
- import { loadSignals, markSignalsAsRead } from '@lumenflow/memory/signal';
10
11
  // Re-export for backwards compatibility (WU-1394)
11
12
  export const HOOK_SCRIPTS = CLAUDE_HOOKS.SCRIPTS;
12
13
  /**
@@ -77,18 +78,9 @@ export function generateEnforcementHooks(config) {
77
78
  },
78
79
  ];
79
80
  }
80
- // WU-1502: Always add PostToolUse Bash dirty-main warning hook
81
- // Detects file modifications on main after Bash commands and emits a warning
82
- postToolUseHooks.push({
83
- matcher: CLAUDE_HOOKS.MATCHERS.BASH,
84
- hooks: [
85
- {
86
- type: 'command',
87
- command: getHookCommand(HOOK_SCRIPTS.WARN_DIRTY_MAIN),
88
- },
89
- ],
90
- });
91
- hooks.postToolUse = postToolUseHooks;
81
+ if (postToolUseHooks.length > 0) {
82
+ hooks.postToolUse = postToolUseHooks;
83
+ }
92
84
  // Always generate PreCompact and SessionStart recovery hooks (WU-1394)
93
85
  // These enable durable context recovery after compaction
94
86
  hooks.preCompact = [
@@ -133,869 +125,13 @@ export function generateEnforcementHooks(config) {
133
125
  ];
134
126
  return hooks;
135
127
  }
136
- /**
137
- * Generate the enforce-worktree.sh hook script content.
138
- *
139
- * WU-1501: Fail-closed on main. Blocks Write/Edit when no active claim context.
140
- * Graceful degradation only when LumenFlow is NOT configured.
141
- * Allowlist: docs/04-operations/tasks/wu/, .lumenflow/, .claude/, plan/
142
- * Branch-PR claimed_mode remains writable from main checkout.
143
- */
144
- export function generateEnforceWorktreeScript() {
145
- // Note: Shell variable escapes (\$, \") are intentional for the generated bash script
146
- /* eslint-disable no-useless-escape */
147
- return `#!/bin/bash
148
- #
149
- # enforce-worktree.sh (WU-1367, WU-1501)
150
- #
151
- # PreToolUse hook that blocks Write/Edit on main checkout.
152
- # WU-1501: Fail-closed - blocks even when no worktrees exist.
153
- # Graceful degradation only when LumenFlow is NOT configured.
154
- #
155
- # Allowlist: docs/04-operations/tasks/wu/, .lumenflow/, .claude/, plan/
156
- # Branch-PR claimed_mode permits writes from main checkout.
157
- #
158
- # Exit codes:
159
- # 0 = Allow operation
160
- # 2 = Block operation (stderr shown to Claude as guidance)
161
- #
162
-
163
- set -euo pipefail
164
-
165
- SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
166
-
167
- # Graceful degradation: if we can't determine state, allow the operation
168
- graceful_allow() {
169
- local reason="\$1"
170
- exit 0
171
- }
172
-
173
- # Derive repo paths from CLAUDE_PROJECT_DIR
174
- if [[ -z "\${CLAUDE_PROJECT_DIR:-}" ]]; then
175
- graceful_allow "CLAUDE_PROJECT_DIR not set"
176
- fi
177
-
178
- MAIN_REPO_PATH="\$CLAUDE_PROJECT_DIR"
179
- WORKTREES_DIR="\${MAIN_REPO_PATH}/worktrees"
180
- LUMENFLOW_DIR="\${MAIN_REPO_PATH}/.lumenflow"
181
-
182
- # Check if .lumenflow exists (LumenFlow is configured)
183
- if [[ ! -d "\$LUMENFLOW_DIR" ]]; then
184
- graceful_allow "No .lumenflow directory (LumenFlow not configured)"
185
- fi
186
-
187
- # Read JSON input from stdin
188
- INPUT=\$(cat)
189
-
190
- if [[ -z "\$INPUT" ]]; then
191
- graceful_allow "No input provided"
192
- fi
193
-
194
- # Parse JSON with Python
195
- TMPFILE=\$(mktemp)
196
- echo "\$INPUT" > "\$TMPFILE"
197
-
198
- PARSE_RESULT=\$(python3 -c "
199
- import json
200
- import sys
201
- try:
202
- with open('\$TMPFILE', 'r') as f:
203
- data = json.load(f)
204
- tool_name = data.get('tool_name', '')
205
- tool_input = data.get('tool_input', {})
206
- if not isinstance(tool_input, dict):
207
- tool_input = {}
208
- file_path = tool_input.get('file_path', '')
209
- print('OK')
210
- print(tool_name if tool_name else '')
211
- print(file_path if file_path else '')
212
- except Exception as e:
213
- print('ERROR')
214
- print(str(e))
215
- print('')
216
- " 2>&1)
217
-
218
- rm -f "\$TMPFILE"
219
-
220
- # Parse the result
221
- PARSE_STATUS=\$(echo "\$PARSE_RESULT" | head -1)
222
- TOOL_NAME=\$(echo "\$PARSE_RESULT" | sed -n '2p')
223
- FILE_PATH=\$(echo "\$PARSE_RESULT" | sed -n '3p')
224
-
225
- if [[ "\$PARSE_STATUS" != "OK" ]]; then
226
- graceful_allow "JSON parse failed"
227
- fi
228
-
229
- # Only process Write and Edit tools
230
- if [[ "\$TOOL_NAME" != "Write" && "\$TOOL_NAME" != "Edit" ]]; then
231
- exit 0
232
- fi
233
-
234
- if [[ -z "\$FILE_PATH" ]]; then
235
- graceful_allow "No file_path in input"
236
- fi
237
-
238
- # Resolve the file path
239
- RESOLVED_PATH=\$(realpath -m "\$FILE_PATH" 2>/dev/null || echo "\$FILE_PATH")
240
-
241
- # Allow if path is outside repo entirely
242
- if [[ "\$RESOLVED_PATH" != "\${MAIN_REPO_PATH}/"* && "\$RESOLVED_PATH" != "\${MAIN_REPO_PATH}" ]]; then
243
- exit 0
244
- fi
245
-
246
- # Allow if path is inside a worktree
247
- if [[ "\$RESOLVED_PATH" == "\${WORKTREES_DIR}/"* ]]; then
248
- exit 0
249
- fi
250
-
251
- # Check if any active worktrees exist
252
- WORKTREE_COUNT=0
253
- if [[ -d "\$WORKTREES_DIR" ]]; then
254
- WORKTREE_COUNT=\$(find "\$WORKTREES_DIR" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | wc -l)
255
- fi
256
-
257
- # If worktrees exist, block writes to main repo (original behavior)
258
- if [[ "\$WORKTREE_COUNT" -gt 0 ]]; then
259
- ACTIVE_WORKTREES=\$(find "\$WORKTREES_DIR" -mindepth 1 -maxdepth 1 -type d -printf '%f\\n' 2>/dev/null | head -5 | tr '\\n' ', ' | sed 's/,\$//')
260
-
261
- echo "" >&2
262
- echo "=== Worktree Enforcement ===" >&2
263
- echo "" >&2
264
- echo "BLOCKED: \$TOOL_NAME to main repo" >&2
265
- echo "" >&2
266
- echo "Active worktrees: \${ACTIVE_WORKTREES:-none detected}" >&2
267
- echo "" >&2
268
- echo "USE INSTEAD:" >&2
269
- echo " 1. cd to your worktree: cd worktrees/<lane>-wu-<id>/" >&2
270
- echo " 2. Make your edits in the worktree" >&2
271
- echo "" >&2
272
- echo "See: LUMENFLOW.md for worktree discipline" >&2
273
- echo "==============================" >&2
274
- exit 2
275
- fi
276
-
277
- # WU-1501: Fail-closed on main when no active worktrees exist
278
- # Check allowlist: paths that are always safe to write on main
279
- RELATIVE_PATH="\${RESOLVED_PATH#\${MAIN_REPO_PATH}/}"
280
-
281
- case "\$RELATIVE_PATH" in
282
- docs/04-operations/tasks/wu/*) exit 0 ;; # WU YAML specs
283
- .lumenflow/*) exit 0 ;; # LumenFlow state/config
284
- .claude/*) exit 0 ;; # Claude Code config
285
- plan/*) exit 0 ;; # Plan/spec scaffolds
286
- esac
287
-
288
- # Check for branch-pr claimed_mode (allows main writes without worktree)
289
- STATE_FILE="\${LUMENFLOW_DIR}/state/wu-events.jsonl"
290
- if [[ -f "\$STATE_FILE" ]]; then
291
- if grep -q '"claimed_mode":"branch-pr"' "\$STATE_FILE" 2>/dev/null; then
292
- if grep -q '"status":"in_progress"' "\$STATE_FILE" 2>/dev/null; then
293
- exit 0 # Branch-PR WU active - allow main writes
294
- fi
295
- fi
296
- fi
297
-
298
- # WU-1501: Fail-closed - no active claim context, block the write
299
- echo "" >&2
300
- echo "=== Worktree Enforcement ===" >&2
301
- echo "" >&2
302
- echo "BLOCKED: \$TOOL_NAME on main (no active WU claim)" >&2
303
- echo "" >&2
304
- echo "No worktrees exist and no branch-pr WU is in progress." >&2
305
- echo "" >&2
306
- echo "WHAT TO DO:" >&2
307
- echo " 1. Claim a WU: pnpm wu:claim --id WU-XXXX --lane \\"<Lane>\\"" >&2
308
- echo " 2. cd worktrees/<lane>-wu-xxxx" >&2
309
- echo " 3. Make your edits in the worktree" >&2
310
- echo "" >&2
311
- echo "See: LUMENFLOW.md for worktree discipline" >&2
312
- echo "==============================" >&2
313
- exit 2
314
- `;
315
- /* eslint-enable no-useless-escape */
316
- }
317
- /**
318
- * Generate the require-wu.sh hook script content.
319
- *
320
- * This hook blocks Write/Edit operations when no WU is claimed.
321
- * Implements graceful degradation: allows operations if LumenFlow
322
- * state cannot be determined.
323
- */
324
- export function generateRequireWuScript() {
325
- // Note: Shell variable escapes (\$, \") are intentional for the generated bash script
326
- /* eslint-disable no-useless-escape */
327
- return `#!/bin/bash
328
- #
329
- # require-wu.sh (WU-1367)
330
- #
331
- # PreToolUse hook that blocks Write/Edit when no WU is claimed.
332
- # Graceful degradation: allows operations if state cannot be determined.
333
- #
334
- # Exit codes:
335
- # 0 = Allow operation
336
- # 2 = Block operation (stderr shown to Claude as guidance)
337
- #
338
-
339
- set -euo pipefail
340
-
341
- SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
342
-
343
- # Graceful degradation
344
- graceful_allow() {
345
- local reason="\$1"
346
- exit 0
347
- }
348
-
349
- if [[ -z "\${CLAUDE_PROJECT_DIR:-}" ]]; then
350
- graceful_allow "CLAUDE_PROJECT_DIR not set"
351
- fi
352
-
353
- MAIN_REPO_PATH="\$CLAUDE_PROJECT_DIR"
354
- WORKTREES_DIR="\${MAIN_REPO_PATH}/worktrees"
355
- LUMENFLOW_DIR="\${MAIN_REPO_PATH}/.lumenflow"
356
- STATE_FILE="\${LUMENFLOW_DIR}/state/wu-events.jsonl"
357
-
358
- # Check if LumenFlow is configured
359
- if [[ ! -d "\$LUMENFLOW_DIR" ]]; then
360
- graceful_allow "No .lumenflow directory"
361
- fi
362
-
363
- # Read JSON input
364
- INPUT=\$(cat)
365
- if [[ -z "\$INPUT" ]]; then
366
- graceful_allow "No input"
367
- fi
368
-
369
- # Parse JSON
370
- TMPFILE=\$(mktemp)
371
- echo "\$INPUT" > "\$TMPFILE"
372
-
373
- TOOL_NAME=\$(python3 -c "
374
- import json
375
- try:
376
- with open('\$TMPFILE', 'r') as f:
377
- data = json.load(f)
378
- print(data.get('tool_name', ''))
379
- except:
380
- print('')
381
- " 2>/dev/null || echo "")
382
-
383
- rm -f "\$TMPFILE"
384
-
385
- # Only check Write and Edit
386
- if [[ "\$TOOL_NAME" != "Write" && "\$TOOL_NAME" != "Edit" ]]; then
387
- exit 0
388
- fi
389
-
390
- # Check for active worktrees (indicates claimed WU)
391
- if [[ -d "\$WORKTREES_DIR" ]]; then
392
- WORKTREE_COUNT=\$(find "\$WORKTREES_DIR" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | wc -l)
393
- if [[ "\$WORKTREE_COUNT" -gt 0 ]]; then
394
- exit 0 # Has worktrees = has claimed WU
395
- fi
396
- fi
397
-
398
- # Check state file for in_progress WUs
399
- if [[ -f "\$STATE_FILE" ]]; then
400
- # Look for any WU with in_progress status
401
- if grep -q '"status":"in_progress"' "\$STATE_FILE" 2>/dev/null; then
402
- exit 0 # Has in_progress WU
403
- fi
404
- fi
405
-
406
- # No claimed WU found
407
- echo "" >&2
408
- echo "=== WU Enforcement ===" >&2
409
- echo "" >&2
410
- echo "BLOCKED: \$TOOL_NAME without claimed WU" >&2
411
- echo "" >&2
412
- echo "You must claim a WU before making edits:" >&2
413
- echo " pnpm wu:claim --id WU-XXXX --lane <Lane>" >&2
414
- echo " cd worktrees/<lane>-wu-xxxx" >&2
415
- echo "" >&2
416
- echo "Or create a new WU:" >&2
417
- echo " pnpm wu:create --lane <Lane> --title \"Description\"" >&2
418
- echo "" >&2
419
- echo "See: LUMENFLOW.md for workflow details" >&2
420
- echo "======================" >&2
421
- exit 2
422
- `;
423
- /* eslint-enable no-useless-escape */
424
- }
425
- /**
426
- * Generate the warn-incomplete.sh hook script content.
427
- *
428
- * This Stop hook warns when session ends without wu:done.
429
- * Always exits 0 (warning only, never blocks).
430
- */
431
- export function generateWarnIncompleteScript() {
432
- // Note: Shell variable escapes (\$, \") are intentional for the generated bash script
433
- /* eslint-disable no-useless-escape */
434
- return `#!/bin/bash
435
- #
436
- # warn-incomplete.sh (WU-1367)
437
- #
438
- # Stop hook that warns when session ends without wu:done.
439
- # This is advisory only - never blocks session termination.
440
- #
441
- # Exit codes:
442
- # 0 = Always (warnings only)
443
- #
444
-
445
- SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
446
-
447
- if [[ -z "\${CLAUDE_PROJECT_DIR:-}" ]]; then
448
- exit 0
449
- fi
450
-
451
- MAIN_REPO_PATH="\$CLAUDE_PROJECT_DIR"
452
- WORKTREES_DIR="\${MAIN_REPO_PATH}/worktrees"
453
-
454
- # Check for active worktrees
455
- if [[ ! -d "\$WORKTREES_DIR" ]]; then
456
- exit 0
457
- fi
458
-
459
- WORKTREE_COUNT=\$(find "\$WORKTREES_DIR" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | wc -l)
460
- if [[ "\$WORKTREE_COUNT" -eq 0 ]]; then
461
- exit 0
462
- fi
463
-
464
- # Get active worktree names
465
- ACTIVE_WORKTREES=\$(find "\$WORKTREES_DIR" -mindepth 1 -maxdepth 1 -type d -printf '%f\\n' 2>/dev/null | head -5 | tr '\\n' ', ' | sed 's/,\$//')
466
-
467
- echo "" >&2
468
- echo "=== Session Completion Reminder ===" >&2
469
- echo "" >&2
470
- echo "You have active worktrees: \$ACTIVE_WORKTREES" >&2
471
- echo "" >&2
472
- echo "If your work is complete, remember to run:" >&2
473
- echo " pnpm wu:prep --id WU-XXXX (from worktree)" >&2
474
- echo " pnpm wu:done --id WU-XXXX (from main)" >&2
475
- echo "" >&2
476
- echo "If work is incomplete, it will be preserved in the worktree." >&2
477
- echo "====================================" >&2
478
-
479
- exit 0
480
- `;
481
- /* eslint-enable no-useless-escape */
482
- }
483
- /**
484
- * WU-1502: Generate the warn-dirty-main.sh hook script content.
485
- *
486
- * PostToolUse hook for the Bash tool that detects file modifications on main
487
- * checkout and emits a high-signal warning with changed paths.
488
- *
489
- * Design:
490
- * - No-op inside worktrees (only fires on main checkout)
491
- * - Uses `git status --porcelain` to detect dirty state
492
- * - Always exits 0 (warning only, never blocks Bash execution)
493
- * - Reads stdin JSON to confirm tool_name is "Bash"
494
- * - Clean working tree overhead target: <50ms
495
- *
496
- * This is a vendor-agnostic detector: the script is generated by the shared
497
- * enforcement generator and placed as a thin wrapper by vendor integrations.
498
- */
499
- export function generateWarnDirtyMainScript() {
500
- // Note: Shell variable escapes (\$, \") are intentional for the generated bash script
501
- /* eslint-disable no-useless-escape */
502
- return `#!/bin/bash
503
- #
504
- # warn-dirty-main.sh (WU-1502)
505
- #
506
- # PostToolUse hook for the Bash tool.
507
- # Detects file modifications on main checkout after Bash commands
508
- # and emits a high-signal warning listing changed paths.
509
- #
510
- # No-op inside worktrees. Always exits 0 (warning only, never blocks).
511
- # Clean working tree overhead: <50ms (single git status call).
512
- #
513
- # Performance: fast-path checks (worktree, branch) run before stdin
514
- # reading to avoid Python overhead in the common no-op case.
515
- #
516
- # Exit codes:
517
- # 0 = Always (warnings only, never blocks)
518
- #
519
-
520
- # Fail-open: errors must never block Bash execution
521
- set +e
522
-
523
- # Derive repo paths
524
- if [[ -z "\\\${CLAUDE_PROJECT_DIR:-}" ]]; then
525
- exit 0
526
- fi
527
-
528
- REPO_PATH="\\\$CLAUDE_PROJECT_DIR"
529
- WORKTREES_DIR="\\\${REPO_PATH}/worktrees"
530
- LUMENFLOW_DIR="\\\${REPO_PATH}/.lumenflow"
531
-
532
- # No-op if LumenFlow is not configured
533
- if [[ ! -d "\\\$LUMENFLOW_DIR" ]]; then
534
- exit 0
535
- fi
536
-
537
- # Fast-path: no-op inside worktrees (avoids stdin/Python overhead)
538
- CWD=\\\$(pwd 2>/dev/null || echo "")
539
- if [[ "\\\$CWD" == "\\\${WORKTREES_DIR}/"* ]]; then
540
- # Drain stdin to prevent broken pipe
541
- cat > /dev/null 2>/dev/null || true
542
- exit 0
543
- fi
544
-
545
- # Fast-path: only warn on main branch (avoids stdin/Python overhead)
546
- CURRENT_BRANCH=\\\$(git -C "\\\$REPO_PATH" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
547
- if [[ "\\\$CURRENT_BRANCH" != "main" ]]; then
548
- # Drain stdin to prevent broken pipe
549
- cat > /dev/null 2>/dev/null || true
550
- exit 0
551
- fi
552
-
553
- # Read JSON input from stdin (PostToolUse provides tool_name + tool_input)
554
- INPUT=\\\$(cat 2>/dev/null || true)
555
-
556
- # Verify this is a Bash tool call (defensive: matcher should already filter)
557
- if [[ -n "\\\$INPUT" ]]; then
558
- TOOL_NAME=\\\$(echo "\\\$INPUT" | python3 -c "
559
- import json, sys
560
- try:
561
- data = json.load(sys.stdin)
562
- print(data.get('tool_name', ''))
563
- except:
564
- print('')
565
- " 2>/dev/null || echo "")
566
-
567
- if [[ "\\\$TOOL_NAME" != "Bash" ]]; then
568
- exit 0
569
- fi
570
- fi
571
-
572
- # Check for dirty working tree (modified/untracked files)
573
- DIRTY_LINES=\\\$(git -C "\\\$REPO_PATH" status --porcelain --untracked-files=all 2>/dev/null || true)
574
- if [[ -z "\\\$DIRTY_LINES" ]]; then
575
- exit 0
576
- fi
577
-
578
- # Emit warning with changed paths
579
- echo "" >&2
580
- echo "=== Dirty Main Warning (WU-1502) ===" >&2
581
- echo "" >&2
582
- echo "WARNING: Bash command modified files on main checkout." >&2
583
- echo "" >&2
584
- echo "Modified paths:" >&2
585
- echo "\\\$DIRTY_LINES" | head -20 | sed 's/^/ /' >&2
586
- LINE_COUNT=\\\$(echo "\\\$DIRTY_LINES" | wc -l | tr -d ' ')
587
- if [[ \\\$LINE_COUNT -gt 20 ]]; then
588
- echo " ... (\\\$LINE_COUNT total, showing first 20)" >&2
589
- fi
590
- echo "" >&2
591
- echo "WHAT TO DO:" >&2
592
- echo " 1. If intentional: claim a WU and move changes to a worktree" >&2
593
- echo " pnpm wu:claim --id WU-XXXX --lane \\"<Lane>\\"" >&2
594
- echo " 2. If accidental: discard the changes" >&2
595
- echo " git checkout -- . && git clean -fd" >&2
596
- echo "" >&2
597
- echo "Main should stay clean. See: LUMENFLOW.md" >&2
598
- echo "=======================================" >&2
599
-
600
- exit 0
601
- `;
602
- /* eslint-enable no-useless-escape */
603
- }
604
- /**
605
- * Generate the pre-compact-checkpoint.sh hook script content.
606
- *
607
- * This PreCompact hook saves a checkpoint and writes a durable recovery file
608
- * before context compaction. The recovery file survives compaction and is
609
- * read by session-start-recovery.sh on the next session start.
610
- *
611
- * Part of WU-1394: Durable recovery pattern for context preservation.
612
- */
613
- export function generatePreCompactCheckpointScript() {
614
- // Note: Shell variable escapes (\$, \") are intentional for the generated bash script
615
- /* eslint-disable no-useless-escape */
616
- return `#!/bin/bash
617
- #
618
- # pre-compact-checkpoint.sh
619
- #
620
- # PreCompact hook - auto-checkpoint + durable recovery marker (WU-1390)
621
- #
622
- # Fires before context compaction to:
623
- # 1. Save a checkpoint with the current WU progress
624
- # 2. Write a durable recovery file that survives compaction
625
- #
626
- # The recovery file is read by session-start-recovery.sh on the next
627
- # session start (after compact, resume, or clear) to restore context.
628
- #
629
- # Exit codes:
630
- # 0 = Always allow (cannot block compaction)
631
- #
632
- # Uses python3 for JSON parsing (consistent with other hooks)
633
- #
634
-
635
- set -euo pipefail
636
-
637
- SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
638
-
639
- # Derive repo paths from CLAUDE_PROJECT_DIR
640
- if [[ -n "\${CLAUDE_PROJECT_DIR:-}" ]]; then
641
- REPO_PATH="\$CLAUDE_PROJECT_DIR"
642
- else
643
- REPO_PATH=\$(git rev-parse --show-toplevel 2>/dev/null || echo "")
644
- if [[ -z "\$REPO_PATH" ]]; then
645
- exit 0
646
- fi
647
- fi
648
-
649
- # Read JSON input from stdin
650
- INPUT=\$(cat)
651
-
652
- # Parse trigger from hook input (defensive - default to "auto")
653
- # PreCompact provides: { "trigger": "manual" | "auto" }
654
- TRIGGER=\$(python3 -c "
655
- import json
656
- import sys
657
- try:
658
- data = json.loads('''\$INPUT''')
659
- trigger = data.get('trigger', 'auto')
660
- print(trigger if trigger else 'auto')
661
- except:
662
- print('auto')
663
- " 2>/dev/null || echo "auto")
664
-
665
- # Get WU ID from worktree context (wu:status --json)
666
- # Location.worktreeWuId is set when in a worktree
667
- WU_ID=\$(pnpm wu:status --json 2>/dev/null | python3 -c "
668
- import json
669
- import sys
670
- try:
671
- data = json.load(sys.stdin)
672
- location = data.get('location', {})
673
- wu_id = location.get('worktreeWuId') or ''
674
- print(wu_id)
675
- except:
676
- print('')
677
- " 2>/dev/null || echo "")
678
-
679
- # Proceed with worktree-based recovery if we have a WU ID
680
- if [[ -n "\$WU_ID" ]]; then
681
- # Save checkpoint with pre-compact trigger
682
- # Note: This may fail if CLI not built, but that's OK - recovery file is more important
683
- pnpm mem:checkpoint "Auto: pre-\${TRIGGER}-compaction" --wu "\$WU_ID" --trigger "pre-compact" --quiet 2>/dev/null || true
684
-
685
- # Write durable recovery marker (survives compaction)
686
- # This is the key mechanism - file persists and is read by session-start-recovery.sh
687
- RECOVERY_DIR="\${REPO_PATH}/.lumenflow/state"
688
- RECOVERY_FILE="\${RECOVERY_DIR}/recovery-pending-\${WU_ID}.md"
689
-
690
- mkdir -p "\$RECOVERY_DIR"
691
-
692
- # Generate recovery context using mem:recover
693
- # The --quiet flag outputs only the recovery context without headers
694
- pnpm mem:recover --wu "\$WU_ID" --quiet > "\$RECOVERY_FILE" 2>/dev/null || {
695
- # Fallback minimal recovery if mem:recover fails
696
- cat > "\$RECOVERY_FILE" << EOF
697
- # POST-COMPACTION RECOVERY
698
-
699
- You are resuming work after context compaction. Your previous context was lost.
700
- **WU:** \${WU_ID}
701
-
702
- ## Next Action
703
- Run \\\`pnpm wu:brief --id \${WU_ID} --client claude-code\\\` to generate a fresh handoff prompt.
704
- EOF
705
- }
706
-
707
- # Output brief warning to stderr (may be compacted away, but recovery file persists)
708
- echo "" >&2
709
- echo "═══════════════════════════════════════════════════════" >&2
710
- echo "⚠️ COMPACTION: Checkpoint saved for \${WU_ID}" >&2
711
- echo "Recovery context: \${RECOVERY_FILE}" >&2
712
- echo "Next: pnpm wu:brief --id \${WU_ID} --client claude-code" >&2
713
- echo "═══════════════════════════════════════════════════════" >&2
714
- else
715
- # WU-1473: Non-worktree orchestrator context recovery
716
- # When not in a worktree (e.g., orchestrator on main), surface unread inbox
717
- # so agents have coordination context after compaction
718
- echo "" >&2
719
- echo "═══════════════════════════════════════════════════════" >&2
720
- echo "⚠️ COMPACTION: No active WU detected (non-worktree context)" >&2
721
- echo "Surfacing recent coordination signals via mem:inbox..." >&2
722
- pnpm mem:inbox --since 1h --quiet 2>/dev/null >&2 || true
723
- echo "═══════════════════════════════════════════════════════" >&2
724
- fi
725
-
726
- # Always exit 0 - cannot block compaction
727
- exit 0
728
- `;
729
- /* eslint-enable no-useless-escape */
730
- }
731
- /**
732
- * Generate the session-start-recovery.sh hook script content.
733
- *
734
- * This SessionStart hook checks for pending recovery files written by
735
- * pre-compact-checkpoint.sh and displays the recovery context to the agent.
736
- * After displaying, the recovery file is deleted (one-time recovery).
737
- *
738
- * Part of WU-1394: Durable recovery pattern for context preservation.
739
- */
740
- export function generateSessionStartRecoveryScript() {
741
- // Note: Shell variable escapes (\$, \") are intentional for the generated bash script
742
- /* eslint-disable no-useless-escape */
743
- return `#!/bin/bash
744
- #
745
- # session-start-recovery.sh
746
- #
747
- # SessionStart hook - check for pending recovery and inject context (WU-1390)
748
- #
749
- # Fires after session start (on compact, resume, or clear) to:
750
- # 1. Check for recovery-pending-*.md files written by pre-compact-checkpoint.sh
751
- # 2. Display the recovery context to the agent
752
- # 3. Remove the recovery file (one-time recovery)
753
- #
754
- # This completes the durable recovery pattern:
755
- # PreCompact writes file → SessionStart reads and deletes it
756
- #
757
- # Exit codes:
758
- # 0 = Always allow (informational hook)
759
- #
760
-
761
- set -euo pipefail
762
-
763
- # Derive repo paths from CLAUDE_PROJECT_DIR
764
- if [[ -n "\${CLAUDE_PROJECT_DIR:-}" ]]; then
765
- REPO_PATH="\$CLAUDE_PROJECT_DIR"
766
- else
767
- REPO_PATH=\$(git rev-parse --show-toplevel 2>/dev/null || echo "")
768
- if [[ -z "\$REPO_PATH" ]]; then
769
- exit 0
770
- fi
771
- fi
772
-
773
- # WU-1505: Early warning for dirty main checkout at SessionStart.
774
- # Informational only (never blocks), helps agents catch polluted main state
775
- # before any work begins.
776
- CWD=\$(pwd)
777
- WORKTREES_DIR="\${REPO_PATH}/worktrees"
778
- CURRENT_BRANCH=\$(git -C "\$REPO_PATH" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
779
-
780
- # No-op in worktrees and non-main branches.
781
- if [[ "\$CWD" != "\${WORKTREES_DIR}/"* ]] && [[ "\$CURRENT_BRANCH" == "main" ]]; then
782
- DIRTY_LINES=\$(git -C "\$REPO_PATH" status --porcelain --untracked-files=all 2>/dev/null || true)
783
- if [[ -n "\$DIRTY_LINES" ]]; then
784
- echo "" >&2
785
- echo "═══════════════════════════════════════════════════════" >&2
786
- echo "⚠️ DIRTY MAIN CHECKOUT DETECTED" >&2
787
- echo "═══════════════════════════════════════════════════════" >&2
788
- echo "" >&2
789
- echo "Uncommitted files in main checkout:" >&2
790
- echo "\$DIRTY_LINES" | head -20 | sed 's/^/ /' >&2
791
- if [[ \$(echo "\$DIRTY_LINES" | wc -l | tr -d ' ') -gt 20 ]]; then
792
- echo " ... (truncated)" >&2
793
- fi
794
- echo "" >&2
795
- echo "Recommended next steps:" >&2
796
- echo " 1. Inspect: git status --short" >&2
797
- echo " 2. Move changes into a WU worktree or commit/discard intentionally" >&2
798
- echo " 3. Keep main clean before starting new work" >&2
799
- echo "" >&2
800
- fi
801
- fi
802
-
803
- RECOVERY_DIR="\${REPO_PATH}/.lumenflow/state"
804
-
805
- # Check if recovery directory exists
806
- if [[ ! -d "\$RECOVERY_DIR" ]]; then
807
- exit 0
808
- fi
809
-
810
- # Find any pending recovery files
811
- FOUND_RECOVERY=false
812
-
813
- for recovery_file in "\$RECOVERY_DIR"/recovery-pending-*.md; do
814
- # Check if glob matched any files (bash glob returns literal pattern if no match)
815
- [[ -f "\$recovery_file" ]] || continue
816
-
817
- FOUND_RECOVERY=true
818
-
819
- # Extract WU ID from filename for display
820
- WU_ID=\$(basename "\$recovery_file" | sed 's/recovery-pending-\\(.*\\)\\.md/\\1/')
821
-
822
- echo "" >&2
823
- echo "═══════════════════════════════════════════════════════" >&2
824
- echo "⚠️ POST-COMPACTION RECOVERY DETECTED" >&2
825
- echo "═══════════════════════════════════════════════════════" >&2
826
- echo "" >&2
827
-
828
- # Display the recovery context
829
- cat "\$recovery_file" >&2
830
-
831
- echo "" >&2
832
- echo "═══════════════════════════════════════════════════════" >&2
833
- echo "" >&2
834
-
835
- # Remove after displaying (one-time recovery)
836
- rm -f "\$recovery_file"
837
- done
838
-
839
- # Additional context if recovery was displayed
840
- if [[ "\$FOUND_RECOVERY" == "true" ]]; then
841
- echo "IMPORTANT: Your context was compacted. Review the recovery info above." >&2
842
- echo "Recommended: Run 'pnpm wu:brief --id \$WU_ID --client claude-code' for fresh full context." >&2
843
- echo "" >&2
844
- fi
845
-
846
- # WU-1473: Surface unread coordination signals for non-worktree orchestrators
847
- # Even without recovery files, agents benefit from seeing recent inbox activity
848
- # This supports orchestrators running from main checkout (not in a worktree)
849
- pnpm mem:inbox --since 1h --unread-only --quiet 2>/dev/null >&2 || true
850
-
851
- exit 0
852
- `;
853
- /* eslint-enable no-useless-escape */
854
- }
855
- /**
856
- * WU-1473: Surface unread signals for agent consumption during claim/start.
857
- *
858
- * Loads all unread signals from the memory layer and returns them for display.
859
- * Implements fail-open: any error returns an empty result without throwing.
860
- *
861
- * @param baseDir - Project base directory
862
- * @returns Unread signal summary (never throws)
863
- */
864
- export async function surfaceUnreadSignals(baseDir) {
865
- try {
866
- const signals = await loadSignals(baseDir, { unreadOnly: true });
867
- return { count: signals.length, signals };
868
- }
869
- catch {
870
- // WU-1473 AC4: Fail-open - memory errors never block lifecycle commands
871
- return { count: 0, signals: [] };
872
- }
873
- }
874
- /**
875
- * WU-1473: Mark all signals for a completed WU as read using receipt-aware behavior.
876
- *
877
- * Loads signals scoped to the given WU ID and marks any unread ones as read
878
- * by appending receipts (WU-1472 pattern). Does not rewrite signals.jsonl.
879
- * Implements fail-open: any error returns zero count without throwing.
880
- *
881
- * @param baseDir - Project base directory
882
- * @param wuId - WU ID whose signals should be marked as read
883
- * @returns Result with count of signals marked (never throws)
884
- */
885
- export async function markCompletedWUSignalsAsRead(baseDir, wuId) {
886
- try {
887
- const signals = await loadSignals(baseDir, { wuId, unreadOnly: true });
888
- if (signals.length === 0) {
889
- return { markedCount: 0 };
890
- }
891
- const signalIds = signals.map((sig) => sig.id);
892
- return await markSignalsAsRead(baseDir, signalIds);
893
- }
894
- catch {
895
- // WU-1473 AC4: Fail-open - memory errors never block lifecycle commands
896
- return { markedCount: 0 };
897
- }
898
- }
899
- export function generateAutoCheckpointScript(intervalToolCalls) {
900
- // Note: Shell variable escapes (\$, \") are intentional for the generated bash script
901
- /* eslint-disable no-useless-escape */
902
- return `#!/bin/bash
903
- #
904
- # auto-checkpoint.sh (WU-1471)
905
- #
906
- # PostToolUse + SubagentStop hook for automatic checkpointing.
907
- # Branches on hook_event_name to decide behavior:
908
- # - PostToolUse: counter-based checkpoint at interval
909
- # - SubagentStop: always checkpoint (sub-agent completed work)
910
- #
911
- # Checkpoint writes are backgrounded in a defensive subshell
912
- # to avoid blocking the agent.
913
- #
914
- # Exit codes:
915
- # 0 = Always (never blocks tool execution)
916
- #
917
-
918
- # Fail-open: any error allows the operation to continue
919
- set +e
920
-
921
- INTERVAL=${intervalToolCalls}
922
-
923
- # Derive repo paths
924
- if [[ -z "\\\${CLAUDE_PROJECT_DIR:-}" ]]; then
925
- exit 0
926
- fi
927
-
928
- REPO_PATH="\\\$CLAUDE_PROJECT_DIR"
929
- LUMENFLOW_DIR="\\\${REPO_PATH}/.lumenflow"
930
- COUNTERS_DIR="\\\${LUMENFLOW_DIR}/state/hook-counters"
931
-
932
- # Check if LumenFlow is configured
933
- if [[ ! -d "\\\$LUMENFLOW_DIR" ]]; then
934
- exit 0
935
- fi
936
-
937
- # Detect WU ID from worktree context
938
- WU_ID=""
939
- CWD=\\\$(pwd 2>/dev/null || echo "")
940
- if [[ "\\\$CWD" == *"/worktrees/"* ]]; then
941
- # Extract WU ID from worktree path (e.g., worktrees/framework-cli-wu-1471)
942
- WORKTREE_NAME=\\\$(basename "\\\$CWD")
943
- WU_ID=\\\$(echo "\\\$WORKTREE_NAME" | grep -oiE 'wu-[0-9]+' | head -1 | tr '[:lower:]' '[:upper:]')
944
- fi
945
-
946
- if [[ -z "\\\$WU_ID" ]]; then
947
- exit 0
948
- fi
949
-
950
- # Determine hook event name (set by Claude Code runtime)
951
- HOOK_EVENT="\\\${hook_event_name:-PostToolUse}"
952
-
953
- # Branch on event type
954
- case "\\\$HOOK_EVENT" in
955
- SubagentStop)
956
- # Always checkpoint when sub-agent stops
957
- (
958
- pnpm mem:checkpoint "Auto: sub-agent completed" --wu "\\\$WU_ID" --trigger "subagent-stop" --quiet 2>/dev/null || true
959
- ) &
960
- ;;
961
- *)
962
- # PostToolUse (default): counter-based checkpointing
963
- mkdir -p "\\\$COUNTERS_DIR" 2>/dev/null || true
964
- COUNTER_FILE="\\\${COUNTERS_DIR}/\\\${WU_ID}.json"
965
-
966
- # Read current count (default 0)
967
- COUNT=0
968
- if [[ -f "\\\$COUNTER_FILE" ]]; then
969
- COUNT=\\\$(python3 -c "
970
- import json
971
- try:
972
- with open('\\\$COUNTER_FILE', 'r') as f:
973
- data = json.load(f)
974
- print(data.get('count', 0))
975
- except:
976
- print(0)
977
- " 2>/dev/null || echo "0")
978
- fi
979
-
980
- # Increment counter
981
- COUNT=\\\$((COUNT + 1))
982
-
983
- # Check if we've reached the interval
984
- if [[ \\\$COUNT -ge \\\$INTERVAL ]]; then
985
- # Reset counter and checkpoint in background
986
- echo '{"count": 0}' > "\\\$COUNTER_FILE" 2>/dev/null || true
987
- (
988
- pnpm mem:checkpoint "Auto: \\\${COUNT} tool calls" --wu "\\\$WU_ID" --trigger "auto-interval" --quiet 2>/dev/null || true
989
- ) &
990
- else
991
- # Just update the counter
992
- echo "{\\\\\\"count\\\\\\": \\\$COUNT}" > "\\\$COUNTER_FILE" 2>/dev/null || true
993
- fi
994
- ;;
995
- esac
996
-
997
- exit 0
998
- `;
999
- /* eslint-enable no-useless-escape */
1000
- }
128
+ // ── Re-exports from per-hook generator modules (WU-1645) ──
129
+ // These preserve the public contract so all existing import paths continue to work.
130
+ export { generateEnforceWorktreeScript } from './generators/enforce-worktree.js';
131
+ export { generateRequireWuScript } from './generators/require-wu.js';
132
+ export { generateWarnIncompleteScript } from './generators/warn-incomplete.js';
133
+ export { generatePreCompactCheckpointScript } from './generators/pre-compact-checkpoint.js';
134
+ export { generateSessionStartRecoveryScript } from './generators/session-start-recovery.js';
135
+ export { generateAutoCheckpointScript } from './generators/auto-checkpoint.js';
136
+ export { surfaceUnreadSignals, markCompletedWUSignalsAsRead, } from './generators/signal-utils.js';
1001
137
  //# sourceMappingURL=enforcement-generator.js.map