@lumenflow/cli 2.18.3 → 2.20.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 (128) hide show
  1. package/README.md +44 -42
  2. package/dist/agent-session.js +1 -1
  3. package/dist/agent-session.js.map +1 -1
  4. package/dist/commands/integrate.js +1 -0
  5. package/dist/commands/integrate.js.map +1 -1
  6. package/dist/commands.js +1 -0
  7. package/dist/commands.js.map +1 -1
  8. package/dist/delegation-list.js +140 -0
  9. package/dist/delegation-list.js.map +1 -0
  10. package/dist/docs-sync.js +1 -0
  11. package/dist/docs-sync.js.map +1 -1
  12. package/dist/doctor.js +36 -99
  13. package/dist/doctor.js.map +1 -1
  14. package/dist/gates-plan-resolvers.js +150 -0
  15. package/dist/gates-plan-resolvers.js.map +1 -0
  16. package/dist/gates-runners.js +533 -0
  17. package/dist/gates-runners.js.map +1 -0
  18. package/dist/gates-types.js +3 -0
  19. package/dist/gates-types.js.map +1 -1
  20. package/dist/gates-utils.js +316 -0
  21. package/dist/gates-utils.js.map +1 -0
  22. package/dist/gates.js +44 -1016
  23. package/dist/gates.js.map +1 -1
  24. package/dist/hooks/enforcement-generator.js +16 -880
  25. package/dist/hooks/enforcement-generator.js.map +1 -1
  26. package/dist/hooks/enforcement-sync.js +6 -5
  27. package/dist/hooks/enforcement-sync.js.map +1 -1
  28. package/dist/hooks/generators/auto-checkpoint.js +123 -0
  29. package/dist/hooks/generators/auto-checkpoint.js.map +1 -0
  30. package/dist/hooks/generators/enforce-worktree.js +188 -0
  31. package/dist/hooks/generators/enforce-worktree.js.map +1 -0
  32. package/dist/hooks/generators/index.js +16 -0
  33. package/dist/hooks/generators/index.js.map +1 -0
  34. package/dist/hooks/generators/pre-compact-checkpoint.js +134 -0
  35. package/dist/hooks/generators/pre-compact-checkpoint.js.map +1 -0
  36. package/dist/hooks/generators/require-wu.js +115 -0
  37. package/dist/hooks/generators/require-wu.js.map +1 -0
  38. package/dist/hooks/generators/session-start-recovery.js +101 -0
  39. package/dist/hooks/generators/session-start-recovery.js.map +1 -0
  40. package/dist/hooks/generators/signal-utils.js +52 -0
  41. package/dist/hooks/generators/signal-utils.js.map +1 -0
  42. package/dist/hooks/generators/warn-incomplete.js +65 -0
  43. package/dist/hooks/generators/warn-incomplete.js.map +1 -0
  44. package/dist/init-detection.js +228 -0
  45. package/dist/init-detection.js.map +1 -0
  46. package/dist/init-scaffolding.js +146 -0
  47. package/dist/init-scaffolding.js.map +1 -0
  48. package/dist/init-templates.js +1928 -0
  49. package/dist/init-templates.js.map +1 -0
  50. package/dist/init.js +137 -2425
  51. package/dist/init.js.map +1 -1
  52. package/dist/initiative-edit.js +42 -11
  53. package/dist/initiative-edit.js.map +1 -1
  54. package/dist/initiative-remove-wu.js +0 -0
  55. package/dist/initiative-status.js +29 -2
  56. package/dist/initiative-status.js.map +1 -1
  57. package/dist/mem-context.js +22 -9
  58. package/dist/mem-context.js.map +1 -1
  59. package/dist/orchestrate-init-status.js +32 -1
  60. package/dist/orchestrate-init-status.js.map +1 -1
  61. package/dist/orchestrate-initiative.js +2 -2
  62. package/dist/orchestrate-initiative.js.map +1 -1
  63. package/dist/orchestrate-monitor.js +38 -38
  64. package/dist/orchestrate-monitor.js.map +1 -1
  65. package/dist/plan-link.js +7 -14
  66. package/dist/plan-link.js.map +1 -1
  67. package/dist/public-manifest.js +19 -5
  68. package/dist/public-manifest.js.map +1 -1
  69. package/dist/shared-validators.js +1 -0
  70. package/dist/shared-validators.js.map +1 -1
  71. package/dist/spawn-list.js +0 -0
  72. package/dist/sync-templates.js +2 -1
  73. package/dist/sync-templates.js.map +1 -1
  74. package/dist/wu-claim-branch.js +121 -0
  75. package/dist/wu-claim-branch.js.map +1 -0
  76. package/dist/wu-claim-output.js +83 -0
  77. package/dist/wu-claim-output.js.map +1 -0
  78. package/dist/wu-claim-resume-handler.js +85 -0
  79. package/dist/wu-claim-resume-handler.js.map +1 -0
  80. package/dist/wu-claim-state.js +572 -0
  81. package/dist/wu-claim-state.js.map +1 -0
  82. package/dist/wu-claim-validation.js +439 -0
  83. package/dist/wu-claim-validation.js.map +1 -0
  84. package/dist/wu-claim-worktree.js +221 -0
  85. package/dist/wu-claim-worktree.js.map +1 -0
  86. package/dist/wu-claim.js +96 -1394
  87. package/dist/wu-claim.js.map +1 -1
  88. package/dist/wu-code-path-coverage.js +81 -0
  89. package/dist/wu-code-path-coverage.js.map +1 -0
  90. package/dist/wu-create-content.js +256 -0
  91. package/dist/wu-create-content.js.map +1 -0
  92. package/dist/wu-create-readiness.js +57 -0
  93. package/dist/wu-create-readiness.js.map +1 -0
  94. package/dist/wu-create-validation.js +124 -0
  95. package/dist/wu-create-validation.js.map +1 -0
  96. package/dist/wu-create.js +45 -442
  97. package/dist/wu-create.js.map +1 -1
  98. package/dist/wu-done.js +151 -249
  99. package/dist/wu-done.js.map +1 -1
  100. package/dist/wu-edit-operations.js +401 -0
  101. package/dist/wu-edit-operations.js.map +1 -0
  102. package/dist/wu-edit-validators.js +280 -0
  103. package/dist/wu-edit-validators.js.map +1 -0
  104. package/dist/wu-edit.js +43 -759
  105. package/dist/wu-edit.js.map +1 -1
  106. package/dist/wu-prep.js +43 -127
  107. package/dist/wu-prep.js.map +1 -1
  108. package/dist/wu-repair.js +1 -1
  109. package/dist/wu-repair.js.map +1 -1
  110. package/dist/wu-sandbox.js +253 -0
  111. package/dist/wu-sandbox.js.map +1 -0
  112. package/dist/wu-spawn-prompt-builders.js +1124 -0
  113. package/dist/wu-spawn-prompt-builders.js.map +1 -0
  114. package/dist/wu-spawn-strategy-resolver.js +319 -0
  115. package/dist/wu-spawn-strategy-resolver.js.map +1 -0
  116. package/dist/wu-spawn.js +9 -1398
  117. package/dist/wu-spawn.js.map +1 -1
  118. package/dist/wu-status.js +4 -0
  119. package/dist/wu-status.js.map +1 -1
  120. package/dist/wu-validate.js +1 -1
  121. package/dist/wu-validate.js.map +1 -1
  122. package/package.json +15 -11
  123. package/templates/core/LUMENFLOW.md.template +29 -99
  124. package/templates/core/UPGRADING.md.template +2 -2
  125. package/templates/core/ai/onboarding/agent-invocation-guide.md.template +1 -1
  126. package/templates/core/ai/onboarding/quick-ref-commands.md.template +29 -4
  127. package/templates/core/ai/onboarding/release-process.md.template +1 -1
  128. package/templates/vendors/claude/.claude/skills/orchestration/SKILL.md.template +8 -8
package/dist/wu-done.js CHANGED
@@ -33,12 +33,16 @@
33
33
  */
34
34
  // WU-2542: Import from @lumenflow/core to establish shim layer dependency
35
35
  import '@lumenflow/core';
36
+ // WU-1663: XState pipeline actor for state-driven orchestration
37
+ import { createActor } from 'xstate';
38
+ import { wuDoneMachine, WU_DONE_EVENTS } from '@lumenflow/core/wu-done-machine';
36
39
  // WU-1153: wu:done guard for uncommitted code_paths is implemented in core package
37
40
  // The guard runs in executeWorktreeCompletion() before metadata transaction
38
41
  // See: packages/@lumenflow/core/src/wu-done-validation.ts
39
42
  import { execSync } from 'node:child_process';
40
43
  import prettyMs from 'pretty-ms';
41
44
  import { runGates } from './gates.js';
45
+ import { resolveWuDonePreCommitGateDecision } from '@lumenflow/core/gates-agent-mode';
42
46
  import { buildClaimRepairCommand } from './wu-claim-repair-guidance.js';
43
47
  import { getGitForCwd, createGitForPath } from '@lumenflow/core/git-adapter';
44
48
  import { die, getErrorMessage } from '@lumenflow/core/error-handler';
@@ -59,9 +63,8 @@ executePreflightCodePathValidation, buildPreflightCodePathErrorMessage,
59
63
  // WU-2308: Pre-commit hooks with worktree context
60
64
  validateAllPreCommitHooks,
61
65
  // WU-2310: Type vs code_paths preflight validation
62
- validateTypeVsCodePathsPreflight, buildTypeVsCodePathsErrorMessage,
63
- // WU-1503: Dirty-main pre-merge guard
64
- validateDirtyMain, buildDirtyMainErrorMessage, } from '@lumenflow/core/wu-done-validators';
66
+ validateTypeVsCodePathsPreflight, buildTypeVsCodePathsErrorMessage, } from '@lumenflow/core/wu-done-validators';
67
+ import { formatPreflightWarnings } from '@lumenflow/core/wu-preflight-validators';
65
68
  // WU-1825: validateCodePathsExist moved to unified code-path-validator
66
69
  import { validateCodePathsExist } from '@lumenflow/core/code-path-validator';
67
70
  import { BRANCHES, REMOTES, PATTERNS, DEFAULTS, LOG_PREFIX, EMOJI, GIT, SESSION, WU_STATUS, WU_EXPOSURE, PKG_MANAGER, SCRIPTS, CLI_FLAGS, FILE_SYSTEM, EXIT_CODES, STRING_LITERALS, MICRO_WORKTREE_OPERATIONS, TELEMETRY_STEPS, SKIP_GATES_REASONS, CHECKPOINT_MESSAGES, LUMENFLOW_PATHS, getWUStatusDisplay,
@@ -97,8 +100,8 @@ import { releaseLaneLock } from '@lumenflow/core/lane-lock';
97
100
  // WU-1747: Checkpoint and lock for concurrent load resilience
98
101
  import { createPreGatesCheckpoint as createWU1747Checkpoint, markGatesPassed, canSkipGates, clearCheckpoint, } from '@lumenflow/core/wu-checkpoint';
99
102
  // WU-1946: Spawn registry for tracking sub-agent spawns
100
- import { SpawnRegistryStore } from '@lumenflow/core/spawn-registry-store';
101
- import { SpawnStatus } from '@lumenflow/core/spawn-registry-schema';
103
+ import { DelegationRegistryStore } from '@lumenflow/core/delegation-registry-store';
104
+ import { DelegationStatus } from '@lumenflow/core/delegation-registry-schema';
102
105
  // WU-1999: Exposure validation for UI pairing
103
106
  // WU-2022: Feature accessibility validation (blocking)
104
107
  import { validateExposure, validateFeatureAccessibility } from '@lumenflow/core/wu-validation';
@@ -454,7 +457,7 @@ export async function enforceSpawnProvenanceForDone(id, doc, options = {}) {
454
457
  const initiativeId = doc.initiative.trim();
455
458
  const baseDir = options.baseDir ?? process.cwd();
456
459
  const force = options.force === true;
457
- const store = new SpawnRegistryStore(path.join(baseDir, '.lumenflow', 'state'));
460
+ const store = new DelegationRegistryStore(path.join(baseDir, '.lumenflow', 'state'));
458
461
  await store.load();
459
462
  const spawnEntry = store.getByTarget(id);
460
463
  if (!spawnEntry) {
@@ -493,7 +496,7 @@ export async function enforceSpawnProvenanceForDone(id, doc, options = {}) {
493
496
  */
494
497
  export async function updateSpawnRegistryOnCompletion(id, baseDir = process.cwd()) {
495
498
  try {
496
- const store = new SpawnRegistryStore(path.join(baseDir, '.lumenflow', 'state'));
499
+ const store = new DelegationRegistryStore(path.join(baseDir, '.lumenflow', 'state'));
497
500
  await store.load();
498
501
  const spawnEntry = store.getByTarget(id);
499
502
  // Graceful skip if no spawn entry found (legacy WU)
@@ -502,7 +505,7 @@ export async function updateSpawnRegistryOnCompletion(id, baseDir = process.cwd(
502
505
  return;
503
506
  }
504
507
  // Update status to completed with completedAt timestamp
505
- await store.updateStatus(spawnEntry.id, SpawnStatus.COMPLETED);
508
+ await store.updateStatus(spawnEntry.id, DelegationStatus.COMPLETED);
506
509
  console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Spawn registry updated: ${id} marked as completed`);
507
510
  }
508
511
  catch (err) {
@@ -655,118 +658,6 @@ async function _ensureCleanWorkingTree() {
655
658
  ` - Leftover changes from previous session`);
656
659
  }
657
660
  }
658
- const INTERNAL_LIFECYCLE_DIRTY_FILES = new Set([
659
- '.lumenflow/flow.log',
660
- '.lumenflow/skip-gates-audit.log',
661
- '.lumenflow/skip-cos-gates-audit.log',
662
- ]);
663
- /**
664
- * WU-1554: Prefixes for metadata files that wu:done manages.
665
- * These files may be dirty after merge+rebase when wu:claim used push-only mode
666
- * or when concurrent agents advance origin/main. Instead of blocking wu:done,
667
- * these are auto-committed by the caller.
668
- */
669
- const METADATA_LIFECYCLE_PREFIXES = [
670
- '.lumenflow/state/',
671
- '.lumenflow/stamps/',
672
- '.lumenflow/archive/',
673
- ];
674
- const PROTECTED_MAIN_LIKE_BRANCHES = new Set([BRANCHES.MAIN, BRANCHES.MASTER]);
675
- export function isProtectedMainLikeBranch(branchName) {
676
- if (!branchName) {
677
- return false;
678
- }
679
- return PROTECTED_MAIN_LIKE_BRANCHES.has(branchName.trim());
680
- }
681
- export function shouldAutoCommitLifecycleWrites(branchName) {
682
- if (!branchName || branchName.trim().length === 0) {
683
- // Fail-safe: unknown branch should never trigger direct lifecycle commits.
684
- return false;
685
- }
686
- return !isProtectedMainLikeBranch(branchName);
687
- }
688
- function parsePorcelainPath(line) {
689
- if (line.length < 4)
690
- return null;
691
- const pathPart = line.slice(3).trim();
692
- if (!pathPart)
693
- return null;
694
- const renameIndex = pathPart.indexOf(' -> ');
695
- if (renameIndex !== -1) {
696
- return pathPart.slice(renameIndex + 4);
697
- }
698
- return pathPart;
699
- }
700
- /**
701
- * WU-1554: Check if a file is a metadata lifecycle file.
702
- * Metadata files are managed by wu:done (wu-events.jsonl, status.md, backlog.md,
703
- * WU YAML, stamps, archives) and may be dirty after merge+rebase.
704
- */
705
- function isMetadataLifecycleFile(filePath, metadataDir) {
706
- if (METADATA_LIFECYCLE_PREFIXES.some((prefix) => filePath.startsWith(prefix))) {
707
- return true;
708
- }
709
- if (metadataDir && filePath.startsWith(metadataDir + '/')) {
710
- return true;
711
- }
712
- return false;
713
- }
714
- /**
715
- * WU-1084: Check for uncommitted changes on main after merge completes.
716
- *
717
- * This catches cases where pnpm format (or other tooling) touched files
718
- * outside the WU's code_paths during worktree work. These changes survive
719
- * the merge and would be silently left behind when the worktree is removed.
720
- *
721
- * WU-1554: Added metadataDir parameter and metadataFiles return field.
722
- * Metadata files (wu-events.jsonl, status.md, backlog.md, WU YAML, stamps)
723
- * may be dirty after merge+rebase when wu:claim used push-only mode.
724
- * These are returned separately for auto-commit by the caller.
725
- *
726
- * @param gitStatus - Output from git status (porcelain format)
727
- * @param wuId - The WU ID for error messaging
728
- * @param metadataDir - Optional task directory prefix for metadata file detection
729
- * @returns Object with isDirty flag, file categories, and optional error message
730
- */
731
- export function checkPostMergeDirtyState(gitStatus, wuId, metadataDir) {
732
- // WU-1522: Split before trimming to preserve leading spaces in porcelain format.
733
- // Porcelain lines like ' M .lumenflow/flow.log' use the leading space as a status
734
- // indicator (working tree vs staging area). Trimming the whole string first strips
735
- // the leading space from the first line, corrupting parsePorcelainPath output.
736
- const lines = gitStatus.split('\n').filter((line) => line.length >= 4);
737
- if (lines.length === 0) {
738
- return { isDirty: false, internalOnlyFiles: [], metadataFiles: [], unrelatedFiles: [] };
739
- }
740
- const dirtyFiles = lines
741
- .map((line) => parsePorcelainPath(line))
742
- .filter((value) => Boolean(value));
743
- // WU-1554: Three-category classification
744
- const internalOnlyFiles = dirtyFiles.filter((file) => INTERNAL_LIFECYCLE_DIRTY_FILES.has(file));
745
- const metadataFiles = dirtyFiles.filter((file) => !INTERNAL_LIFECYCLE_DIRTY_FILES.has(file) && isMetadataLifecycleFile(file, metadataDir));
746
- const unrelatedFiles = dirtyFiles.filter((file) => !INTERNAL_LIFECYCLE_DIRTY_FILES.has(file) && !isMetadataLifecycleFile(file, metadataDir));
747
- if (unrelatedFiles.length === 0) {
748
- return { isDirty: false, internalOnlyFiles, metadataFiles, unrelatedFiles: [] };
749
- }
750
- const displayStatus = gitStatus.trim();
751
- const error = `Main branch has uncommitted changes after merge:\n\n${displayStatus}\n\n` +
752
- `This indicates files were modified outside the WU's code_paths.\n` +
753
- `Common cause: pnpm format touched files outside the WU scope.\n\n` +
754
- `The worktree has NOT been removed to allow investigation.\n\n` +
755
- `Options:\n` +
756
- ` 1. Review and commit the changes: git add . && git commit -m "format: fix formatting"\n` +
757
- ` 2. Discard if unwanted: git checkout -- .\n` +
758
- ` 3. Then re-run: pnpm wu:done --id ${wuId} --skip-worktree-completion`;
759
- return { isDirty: true, internalOnlyFiles, metadataFiles, unrelatedFiles, error };
760
- }
761
- /**
762
- * Build the list of lifecycle files that are safe to restore after a failed wu:done attempt.
763
- *
764
- * On failure, wu:done can leave internal lifecycle logs and metadata lifecycle files dirty on main.
765
- * These files are managed by lifecycle tooling and should not block re-claim/retry.
766
- */
767
- export function getLifecycleRestoreTargetsOnFailure(dirtyState) {
768
- return Array.from(new Set([...dirtyState.internalOnlyFiles, ...dirtyState.metadataFiles]));
769
- }
770
661
  /**
771
662
  * Extract completed WU IDs from git log output.
772
663
  * @param {string} logOutput - Git log output (one commit per line)
@@ -1663,6 +1554,12 @@ async function executePreFlightChecks({ id, args, isBranchOnly, isDocsOnly, docM
1663
1554
  const errorMessage = buildPreflightCodePathErrorMessage(id, preflightResult);
1664
1555
  die(errorMessage);
1665
1556
  }
1557
+ if (Array.isArray(preflightResult.warnings) && preflightResult.warnings.length > 0) {
1558
+ const warningLines = formatPreflightWarnings(preflightResult.warnings, `${LOG_PREFIX.DONE} ${EMOJI.WARNING} Reality preflight warnings:`);
1559
+ for (const line of warningLines) {
1560
+ console.log(line.startsWith(' - ') ? `${LOG_PREFIX.DONE} ${line}` : line);
1561
+ }
1562
+ }
1666
1563
  // WU-2310: Preflight type vs code_paths validation
1667
1564
  // Run BEFORE transaction to prevent documentation WUs with code paths from failing at git commit
1668
1565
  console.log(`${LOG_PREFIX.DONE} Validating type vs code_paths (WU-2310)...`);
@@ -1710,42 +1607,6 @@ async function executePreFlightChecks({ id, args, isBranchOnly, isDocsOnly, docM
1710
1607
  else {
1711
1608
  // Worktree mode: must be on main
1712
1609
  await ensureOnMain(getGitForCwd());
1713
- // WU-1503: Dirty-main pre-merge guard (replaces blanket ensureCleanWorkingTree)
1714
- // Distinguishes between WU-related dirty files (allowed) and unrelated dirty
1715
- // files (blocked with actionable guidance). Uses --force for audited bypass.
1716
- {
1717
- const gitAdapter = getGitForCwd();
1718
- const gitStatus = await gitAdapter.raw(['status', '--porcelain']);
1719
- if (gitStatus && gitStatus.trim()) {
1720
- const wuCodePaths = docForValidation.code_paths || [];
1721
- const dirtyResult = validateDirtyMain(gitStatus, id, wuCodePaths);
1722
- if (!dirtyResult.valid) {
1723
- if (args.force) {
1724
- console.log(`\n${LOG_PREFIX.DONE} ${EMOJI.WARNING} WU-1503: Dirty-main guard bypassed with --force`);
1725
- console.log(`${LOG_PREFIX.DONE} Unrelated dirty files (${dirtyResult.unrelatedFiles.length}):`);
1726
- for (const f of dirtyResult.unrelatedFiles) {
1727
- console.log(`${LOG_PREFIX.DONE} - ${f}`);
1728
- }
1729
- }
1730
- else {
1731
- die(buildDirtyMainErrorMessage(id, dirtyResult.unrelatedFiles));
1732
- }
1733
- }
1734
- else if (dirtyResult.relatedFiles.length > 0) {
1735
- console.log(`${LOG_PREFIX.DONE} ${EMOJI.INFO} WU-1503: ${dirtyResult.relatedFiles.length} related dirty file(s) on main (allowed)`);
1736
- // WU-1554: Auto-restore related dirty files so ff-only merge can proceed.
1737
- // These files will be overwritten by the merge commit anyway.
1738
- // Without this, git merge --ff-only refuses to overwrite dirty tracked files.
1739
- try {
1740
- await gitAdapter.raw(['checkout', '--', ...dirtyResult.relatedFiles]);
1741
- console.log(`${LOG_PREFIX.DONE} ${EMOJI.INFO} WU-1554: Auto-restored ${dirtyResult.relatedFiles.length} related file(s) for clean merge`);
1742
- }
1743
- catch (restoreErr) {
1744
- console.warn(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} WU-1554: Could not auto-restore related files: ${restoreErr.message}`);
1745
- }
1746
- }
1747
- }
1748
- }
1749
1610
  // Prevent coordination failures by ensuring main is up-to-date
1750
1611
  await ensureMainUpToDate();
1751
1612
  // P0 EMERGENCY FIX Part 1: Restore wu-events.jsonl BEFORE parallel completion check
@@ -1939,10 +1800,17 @@ async function executePreFlightChecks({ id, args, isBranchOnly, isDocsOnly, docM
1939
1800
  return { title, docForValidation };
1940
1801
  }
1941
1802
  async function executeGates({ id, args, isBranchOnly, isDocsOnly, worktreePath, branchName, }) {
1803
+ const gateResult = {
1804
+ fullGatesRanInCurrentRun: false,
1805
+ skippedByCheckpoint: false,
1806
+ checkpointId: null,
1807
+ };
1942
1808
  // WU-1747: Check if gates can be skipped based on valid checkpoint
1943
1809
  // This allows resuming wu:done without re-running gates if nothing changed
1944
1810
  const skipResult = canSkipGates(id, { currentHeadSha: undefined });
1945
1811
  if (skipResult.canSkip) {
1812
+ gateResult.skippedByCheckpoint = true;
1813
+ gateResult.checkpointId = skipResult.checkpoint.checkpointId;
1946
1814
  console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} ${CHECKPOINT_MESSAGES.SKIPPING_GATES_VALID}`);
1947
1815
  console.log(`${LOG_PREFIX.DONE} ${CHECKPOINT_MESSAGES.CHECKPOINT_LABEL}: ${skipResult.checkpoint.checkpointId}`);
1948
1816
  console.log(`${LOG_PREFIX.DONE} ${CHECKPOINT_MESSAGES.GATES_PASSED_AT}: ${skipResult.checkpoint.gatesPassedAt}`);
@@ -1954,7 +1822,7 @@ async function executeGates({ id, args, isBranchOnly, isDocsOnly, worktreePath,
1954
1822
  reason: SKIP_GATES_REASONS.CHECKPOINT_VALID,
1955
1823
  checkpoint_id: skipResult.checkpoint.checkpointId,
1956
1824
  });
1957
- return; // Skip gates entirely
1825
+ return gateResult; // Skip gates entirely
1958
1826
  }
1959
1827
  // WU-1747: Create checkpoint before gates for resumption on failure
1960
1828
  if (worktreePath && branchName) {
@@ -2071,6 +1939,7 @@ async function executeGates({ id, args, isBranchOnly, isDocsOnly, worktreePath,
2071
1939
  });
2072
1940
  die(`Gates failed in Branch-Only mode. Fix issues and try again.`);
2073
1941
  }
1942
+ gateResult.fullGatesRanInCurrentRun = true;
2074
1943
  }
2075
1944
  else if (worktreePath && existsSync(worktreePath)) {
2076
1945
  // Worktree mode: run gates in the dedicated worktree
@@ -2079,6 +1948,7 @@ async function executeGates({ id, args, isBranchOnly, isDocsOnly, worktreePath,
2079
1948
  isDocsOnly,
2080
1949
  docsOnly: Boolean(args.docsOnly),
2081
1950
  });
1951
+ gateResult.fullGatesRanInCurrentRun = true;
2082
1952
  }
2083
1953
  else {
2084
1954
  die(`Worktree not found (${worktreePath || 'unknown'}). Gates must run in the lane worktree.\n` +
@@ -2136,6 +2006,7 @@ async function executeGates({ id, args, isBranchOnly, isDocsOnly, worktreePath,
2136
2006
  // WU-1747: Mark checkpoint as gates passed for resumption on failure
2137
2007
  // This allows subsequent wu:done attempts to skip gates if nothing changed
2138
2008
  markGatesPassed(id);
2009
+ return gateResult;
2139
2010
  }
2140
2011
  /**
2141
2012
  * Print State HUD for visibility
@@ -2194,6 +2065,29 @@ async function main() {
2194
2065
  isBranchPR, derivedWorktree, docForValidation: initialDocForValidation, isDocsOnly, } = pathInfo;
2195
2066
  // Capture main checkout path once. process.cwd() may drift later during recovery flows.
2196
2067
  const mainCheckoutPath = process.cwd();
2068
+ // WU-1663: Determine prepPassed early for pipeline actor input.
2069
+ // canSkipGates checks if wu:prep already ran gates successfully via checkpoint.
2070
+ // This drives the isPrepPassed guard on the GATES_SKIPPED transition.
2071
+ const earlySkipResult = canSkipGates(id, { currentHeadSha: undefined });
2072
+ const prepPassed = earlySkipResult.canSkip;
2073
+ // WU-1663: Create XState pipeline actor for state-driven orchestration.
2074
+ // The actor tracks which pipeline stage we're in (validating, gating, committing, etc.)
2075
+ // and provides explicit state/transition contracts. Existing procedural logic continues
2076
+ // to do the real work; the actor provides structured state tracking alongside it.
2077
+ const pipelineActor = createActor(wuDoneMachine, {
2078
+ input: {
2079
+ wuId: id,
2080
+ worktreePath: derivedWorktree,
2081
+ prepPassed,
2082
+ },
2083
+ });
2084
+ pipelineActor.start();
2085
+ // WU-1663: Send START event to transition from idle -> validating
2086
+ pipelineActor.send({
2087
+ type: WU_DONE_EVENTS.START,
2088
+ wuId: id,
2089
+ worktreePath: derivedWorktree || '',
2090
+ });
2197
2091
  // WU-1590: branch-pr has no worktree, treat like branch-only for path resolution and ensureOnMain skip
2198
2092
  const isNoWorktreeMode = isBranchOnly || isBranchPR;
2199
2093
  const resolvedWorktreePath = derivedWorktree && !isNoWorktreeMode
@@ -2220,18 +2114,32 @@ async function main() {
2220
2114
  await ensureCleanWorktree(effectiveWorktreePath);
2221
2115
  }
2222
2116
  // Pre-flight checks (WU-1215: extracted to executePreFlightChecks function)
2223
- const preFlightResult = await executePreFlightChecks({
2224
- id,
2225
- args,
2226
- isBranchOnly: effectiveBranchOnly,
2227
- isDocsOnly,
2228
- docMain,
2229
- docForValidation: initialDocForValidation,
2230
- derivedWorktree: effectiveDerivedWorktree,
2231
- });
2117
+ // WU-1663: Wrap in try/catch to send pipeline failure event before die() propagates
2118
+ let preFlightResult;
2119
+ try {
2120
+ preFlightResult = await executePreFlightChecks({
2121
+ id,
2122
+ args,
2123
+ isBranchOnly: effectiveBranchOnly,
2124
+ isDocsOnly,
2125
+ docMain,
2126
+ docForValidation: initialDocForValidation,
2127
+ derivedWorktree: effectiveDerivedWorktree,
2128
+ });
2129
+ }
2130
+ catch (preFlightErr) {
2131
+ pipelineActor.send({
2132
+ type: WU_DONE_EVENTS.VALIDATION_FAILED,
2133
+ error: getErrorMessage(preFlightErr),
2134
+ });
2135
+ pipelineActor.stop();
2136
+ throw preFlightErr;
2137
+ }
2232
2138
  const title = preFlightResult.title;
2233
2139
  // Note: docForValidation is returned but not used after pre-flight checks
2234
2140
  // The metadata transaction uses docForUpdate instead
2141
+ // WU-1663: Pre-flight checks passed - transition to preparing state
2142
+ pipelineActor.send({ type: WU_DONE_EVENTS.VALIDATION_PASSED });
2235
2143
  // WU-1599: Enforce auditable spawn provenance for initiative-governed WUs.
2236
2144
  await enforceSpawnProvenanceForDone(id, docMain, {
2237
2145
  baseDir: mainCheckoutPath,
@@ -2272,7 +2180,35 @@ async function main() {
2272
2180
  }
2273
2181
  // Otherwise silently allow - fail-open
2274
2182
  }
2275
- await executeGates({ id, args, isBranchOnly: effectiveBranchOnly, isDocsOnly, worktreePath });
2183
+ // WU-1663: Preparation complete - transition to gating state
2184
+ pipelineActor.send({ type: WU_DONE_EVENTS.PREPARATION_COMPLETE });
2185
+ // WU-1663: Wrap gates in try/catch to send pipeline failure event
2186
+ let gateExecutionResult;
2187
+ try {
2188
+ gateExecutionResult = await executeGates({
2189
+ id,
2190
+ args,
2191
+ isBranchOnly: effectiveBranchOnly,
2192
+ isDocsOnly,
2193
+ worktreePath,
2194
+ });
2195
+ }
2196
+ catch (gateErr) {
2197
+ pipelineActor.send({
2198
+ type: WU_DONE_EVENTS.GATES_FAILED,
2199
+ error: getErrorMessage(gateErr),
2200
+ });
2201
+ pipelineActor.stop();
2202
+ throw gateErr;
2203
+ }
2204
+ // WU-1663: Gates passed - transition from gating state.
2205
+ // Use GATES_SKIPPED if checkpoint dedup allowed skip, GATES_PASSED otherwise.
2206
+ if (gateExecutionResult.skippedByCheckpoint) {
2207
+ pipelineActor.send({ type: WU_DONE_EVENTS.GATES_SKIPPED });
2208
+ }
2209
+ else {
2210
+ pipelineActor.send({ type: WU_DONE_EVENTS.GATES_PASSED });
2211
+ }
2276
2212
  // Print State HUD for visibility (WU-1215: extracted to printStateHUD function)
2277
2213
  printStateHUD({
2278
2214
  id,
@@ -2282,12 +2218,17 @@ async function main() {
2282
2218
  derivedWorktree: effectiveDerivedWorktree,
2283
2219
  STAMPS_DIR,
2284
2220
  });
2285
- // Step 0.5: Pre-flight validation - run ALL pre-commit hooks BEFORE merge
2286
- // This prevents partial completion states where merge succeeds but commit fails
2287
- // Validates all 8 gates: secrets, file size, ESLint, Prettier, TypeScript, audit, architecture, tasks
2288
- // WU-2308: Pass worktreePath to run audit from worktree (checks fixed deps, not stale main deps)
2289
- // WU-1145: Skip pre-flight when skipGates is true (pre-flight runs gates which was already skipped)
2290
- if (!args.skipGates) {
2221
+ // Step 0.5: Pre-flight hook validation policy.
2222
+ // WU-1659: Reuse Step 0 gate attestation/checkpoint and avoid duplicate full-suite execution.
2223
+ const preCommitGateDecision = resolveWuDonePreCommitGateDecision({
2224
+ skipGates: Boolean(args.skipGates),
2225
+ fullGatesRanInCurrentRun: gateExecutionResult.fullGatesRanInCurrentRun,
2226
+ skippedByCheckpoint: gateExecutionResult.skippedByCheckpoint,
2227
+ checkpointId: gateExecutionResult.checkpointId,
2228
+ });
2229
+ console.log(`${LOG_PREFIX.DONE} ${preCommitGateDecision.message}`);
2230
+ // Fallback path remains available if gate attestation is missing for any reason.
2231
+ if (preCommitGateDecision.runPreCommitFullSuite) {
2291
2232
  const hookResult = await validateAllPreCommitHooks(id, worktreePath, {
2292
2233
  runGates: ({ cwd }) => runGates({ cwd, docsOnly: false }),
2293
2234
  });
@@ -2295,9 +2236,6 @@ async function main() {
2295
2236
  die('Pre-flight validation failed. Fix hook issues and try again.');
2296
2237
  }
2297
2238
  }
2298
- else {
2299
- console.log(`${LOG_PREFIX.DONE} Skipping pre-flight hook validation (--skip-gates)`);
2300
- }
2301
2239
  // Step 0.6: WU-1781 - Run tasks:validate preflight BEFORE any merge/push operations
2302
2240
  // This prevents deadlocks where validation fails after merge, leaving local main ahead of origin
2303
2241
  // Specifically catches stamp-status mismatches from legacy WUs that would block pre-push hooks
@@ -2358,6 +2296,12 @@ async function main() {
2358
2296
  };
2359
2297
  completionResult = await executeWorktreeCompletion(worktreeContext);
2360
2298
  }
2299
+ // WU-1663: Mode-specific completion succeeded - send pipeline events.
2300
+ // The completion modules handle commit, merge, and push internally.
2301
+ // We send the corresponding pipeline events based on the completion result.
2302
+ pipelineActor.send({ type: WU_DONE_EVENTS.COMMIT_COMPLETE });
2303
+ pipelineActor.send({ type: WU_DONE_EVENTS.MERGE_COMPLETE });
2304
+ pipelineActor.send({ type: WU_DONE_EVENTS.PUSH_COMPLETE });
2361
2305
  // Handle recovery mode (zombie state cleanup completed)
2362
2306
  if ('recovered' in completionResult && completionResult.recovered) {
2363
2307
  // P0 FIX: Release lane lock before early exit
@@ -2369,30 +2313,28 @@ async function main() {
2369
2313
  catch {
2370
2314
  // Intentionally ignore lock release errors during cleanup
2371
2315
  }
2316
+ pipelineActor.stop();
2372
2317
  process.exit(EXIT_CODES.SUCCESS);
2373
2318
  }
2374
2319
  }
2375
2320
  catch (err) {
2376
- // WU-1624: Failure-path lifecycle cleanup
2377
- // When mode execution fails before reaching post-merge cleanup, lifecycle metadata
2378
- // (especially wu-events.jsonl) can remain dirty on main and block future claims.
2379
- // Restore lifecycle-managed files before exiting so retries/re-claims can proceed.
2380
- if (!isBranchPR && !effectiveBranchOnly) {
2381
- try {
2382
- const gitMainForFailureCleanup = getGitForCwd();
2383
- const failureStatus = await gitMainForFailureCleanup.getStatus();
2384
- const metadataDir = path.dirname(STATUS_PATH);
2385
- const failureDirty = checkPostMergeDirtyState(failureStatus, id, metadataDir);
2386
- const restoreTargets = getLifecycleRestoreTargetsOnFailure(failureDirty);
2387
- if (restoreTargets.length > 0) {
2388
- await gitMainForFailureCleanup.raw(['restore', '--', ...restoreTargets]);
2389
- console.log(`${LOG_PREFIX.DONE} ${EMOJI.INFO} Restored lifecycle files after failed completion: ${restoreTargets.join(', ')}`);
2390
- }
2391
- }
2392
- catch (cleanupErr) {
2393
- console.warn(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Could not restore lifecycle files after failure: ${cleanupErr.message}`);
2394
- }
2395
- }
2321
+ // WU-1663: Mode execution failed - determine which stage failed
2322
+ // based on completion result flags and send appropriate failure event.
2323
+ const failureStage = completionResult.committed === false
2324
+ ? WU_DONE_EVENTS.COMMIT_FAILED
2325
+ : completionResult.merged === false
2326
+ ? WU_DONE_EVENTS.MERGE_FAILED
2327
+ : completionResult.pushed === false
2328
+ ? WU_DONE_EVENTS.PUSH_FAILED
2329
+ : WU_DONE_EVENTS.COMMIT_FAILED; // Default to commit as earliest possible failure
2330
+ pipelineActor.send({
2331
+ type: failureStage,
2332
+ error: getErrorMessage(err),
2333
+ });
2334
+ // WU-1663: Log pipeline state for diagnostics
2335
+ const failedSnapshot = pipelineActor.getSnapshot();
2336
+ console.error(`${LOG_PREFIX.DONE} Pipeline state: ${failedSnapshot.value} (failedAt: ${failedSnapshot.context.failedAt})`);
2337
+ pipelineActor.stop();
2396
2338
  // P0 FIX: Release lane lock before error exit
2397
2339
  try {
2398
2340
  const lane = docMain.lane;
@@ -2402,6 +2344,8 @@ async function main() {
2402
2344
  catch {
2403
2345
  // Intentionally ignore lock release errors during error handling
2404
2346
  }
2347
+ console.error(`\n${LOG_PREFIX.DONE} ${EMOJI.FAILURE} Mode execution failed: ${getErrorMessage(err)}`);
2348
+ console.error(`${LOG_PREFIX.DONE} ${EMOJI.INFO} Next step: resolve the reported error and retry: pnpm wu:done --id ${id}`);
2405
2349
  // WU-1811: Check if cleanup is safe before removing worktree
2406
2350
  // If cleanupSafe is false (or undefined), preserve worktree for recovery
2407
2351
  if (err.cleanupSafe === false) {
@@ -2415,58 +2359,6 @@ async function main() {
2415
2359
  else {
2416
2360
  await ensureNoAutoStagedOrNoop([WU_PATH, STATUS_PATH, BACKLOG_PATH, STAMPS_DIR]);
2417
2361
  }
2418
- // WU-1084: Check for uncommitted changes on main after merge
2419
- // WU-1554: Pass metadataDir so metadata files are classified separately
2420
- const gitMain = getGitForCwd();
2421
- const currentBranch = await gitMain.getCurrentBranch();
2422
- const allowLifecycleAutoCommit = shouldAutoCommitLifecycleWrites(currentBranch);
2423
- const metadataDir = path.dirname(STATUS_PATH);
2424
- const postMergeStatus = await gitMain.getStatus();
2425
- const dirtyCheck = checkPostMergeDirtyState(postMergeStatus, id, metadataDir);
2426
- if (dirtyCheck.internalOnlyFiles.length > 0) {
2427
- try {
2428
- await gitMain.raw(['restore', '--', ...dirtyCheck.internalOnlyFiles]);
2429
- console.log(`${LOG_PREFIX.DONE} ${EMOJI.INFO} Restored internal lifecycle log files: ${dirtyCheck.internalOnlyFiles.join(', ')}`);
2430
- }
2431
- catch (restoreErr) {
2432
- console.warn(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Could not auto-restore internal lifecycle files: ${restoreErr.message}`);
2433
- }
2434
- }
2435
- // WU-1554: Auto-commit metadata files left dirty after merge+rebase
2436
- // This handles push-only claim mode and concurrent agent advancement
2437
- if (dirtyCheck.metadataFiles.length > 0) {
2438
- if (allowLifecycleAutoCommit) {
2439
- try {
2440
- await gitMain.add(dirtyCheck.metadataFiles);
2441
- await gitMain.commit(`chore: post-merge metadata sync for ${id} [skip ci]`);
2442
- await gitMain.raw(['pull', '--rebase', '--autostash', 'origin', currentBranch]);
2443
- await gitMain.push('origin', currentBranch);
2444
- console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Auto-committed post-merge metadata: ${dirtyCheck.metadataFiles.join(', ')}`);
2445
- }
2446
- catch (commitErr) {
2447
- console.warn(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Could not auto-commit metadata files: ${commitErr.message}`);
2448
- }
2449
- }
2450
- else {
2451
- try {
2452
- await gitMain.raw(['restore', '--', ...dirtyCheck.metadataFiles]);
2453
- console.log(`${LOG_PREFIX.DONE} ${EMOJI.INFO} Protected branch ${currentBranch}: restored lifecycle metadata files instead of committing on branch`);
2454
- }
2455
- catch (restoreErr) {
2456
- console.warn(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Could not restore metadata lifecycle files on protected branch ${currentBranch}: ${restoreErr.message}`);
2457
- }
2458
- }
2459
- }
2460
- const postLifecycleStatus = await gitMain.getStatus();
2461
- const postLifecycleDirty = checkPostMergeDirtyState(postLifecycleStatus, id, metadataDir);
2462
- if (!allowLifecycleAutoCommit && postLifecycleDirty.metadataFiles.length > 0) {
2463
- die(`Protected branch ${currentBranch} still has lifecycle metadata changes after wu:done:\n` +
2464
- `${postLifecycleDirty.metadataFiles.join(STRING_LITERALS.NEWLINE)}\n\n` +
2465
- `wu:done will not commit directly on ${currentBranch}. Resolve and retry from the WU worktree/lane branch.`);
2466
- }
2467
- if (postLifecycleDirty.isDirty) {
2468
- die(postLifecycleDirty.error);
2469
- }
2470
2362
  // Step 6 & 7: Cleanup (remove worktree, delete branch) - WU-1215
2471
2363
  // WU-1811: Only run cleanup if all completion steps succeeded
2472
2364
  if (completionResult.cleanupSafe !== false) {
@@ -2539,6 +2431,12 @@ async function main() {
2539
2431
  // Double fail-open: even if runDecayOnDone itself throws unexpectedly, never block wu:done
2540
2432
  console.warn(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Decay archival error (fail-open): ${getErrorMessage(err)}`);
2541
2433
  }
2434
+ // WU-1663: Cleanup complete - transition to final done state
2435
+ pipelineActor.send({ type: WU_DONE_EVENTS.CLEANUP_COMPLETE });
2436
+ // WU-1663: Log final pipeline state for diagnostics
2437
+ const finalSnapshot = pipelineActor.getSnapshot();
2438
+ console.log(`${LOG_PREFIX.DONE} Pipeline state: ${finalSnapshot.value} (WU-1663)`);
2439
+ pipelineActor.stop();
2542
2440
  console.log(`\n${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Transaction COMMIT - all steps succeeded (WU-755)`);
2543
2441
  console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Marked done, pushed, and cleaned up.`);
2544
2442
  console.log(`- WU: ${id} — ${title}`);
@@ -2555,7 +2453,11 @@ async function main() {
2555
2453
  // WU-1983: Migration deployment nudge - only if supabase paths in code_paths
2556
2454
  const codePaths = docMain.code_paths || [];
2557
2455
  await printMigrationDeploymentNudge(codePaths, mainCheckoutPath);
2558
- if (allowLifecycleAutoCommit) {
2456
+ const currentBranch = (await getGitForCwd().getCurrentBranch()).trim();
2457
+ const shouldRunCleanupMutations = currentBranch.length > 0 &&
2458
+ currentBranch !== BRANCHES.MAIN &&
2459
+ currentBranch !== BRANCHES.MASTER;
2460
+ if (shouldRunCleanupMutations) {
2559
2461
  // WU-1366: Auto state cleanup after successful completion
2560
2462
  // Non-fatal: errors are logged but do not block completion
2561
2463
  await runAutoCleanupAfterDone(mainCheckoutPath);