@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
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,7 @@ 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';
65
67
  // WU-1825: validateCodePathsExist moved to unified code-path-validator
66
68
  import { validateCodePathsExist } from '@lumenflow/core/code-path-validator';
67
69
  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 +99,8 @@ import { releaseLaneLock } from '@lumenflow/core/lane-lock';
97
99
  // WU-1747: Checkpoint and lock for concurrent load resilience
98
100
  import { createPreGatesCheckpoint as createWU1747Checkpoint, markGatesPassed, canSkipGates, clearCheckpoint, } from '@lumenflow/core/wu-checkpoint';
99
101
  // 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';
102
+ import { DelegationRegistryStore } from '@lumenflow/core/delegation-registry-store';
103
+ import { DelegationStatus } from '@lumenflow/core/delegation-registry-schema';
102
104
  // WU-1999: Exposure validation for UI pairing
103
105
  // WU-2022: Feature accessibility validation (blocking)
104
106
  import { validateExposure, validateFeatureAccessibility } from '@lumenflow/core/wu-validation';
@@ -454,7 +456,7 @@ export async function enforceSpawnProvenanceForDone(id, doc, options = {}) {
454
456
  const initiativeId = doc.initiative.trim();
455
457
  const baseDir = options.baseDir ?? process.cwd();
456
458
  const force = options.force === true;
457
- const store = new SpawnRegistryStore(path.join(baseDir, '.lumenflow', 'state'));
459
+ const store = new DelegationRegistryStore(path.join(baseDir, '.lumenflow', 'state'));
458
460
  await store.load();
459
461
  const spawnEntry = store.getByTarget(id);
460
462
  if (!spawnEntry) {
@@ -493,7 +495,7 @@ export async function enforceSpawnProvenanceForDone(id, doc, options = {}) {
493
495
  */
494
496
  export async function updateSpawnRegistryOnCompletion(id, baseDir = process.cwd()) {
495
497
  try {
496
- const store = new SpawnRegistryStore(path.join(baseDir, '.lumenflow', 'state'));
498
+ const store = new DelegationRegistryStore(path.join(baseDir, '.lumenflow', 'state'));
497
499
  await store.load();
498
500
  const spawnEntry = store.getByTarget(id);
499
501
  // Graceful skip if no spawn entry found (legacy WU)
@@ -502,7 +504,7 @@ export async function updateSpawnRegistryOnCompletion(id, baseDir = process.cwd(
502
504
  return;
503
505
  }
504
506
  // Update status to completed with completedAt timestamp
505
- await store.updateStatus(spawnEntry.id, SpawnStatus.COMPLETED);
507
+ await store.updateStatus(spawnEntry.id, DelegationStatus.COMPLETED);
506
508
  console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Spawn registry updated: ${id} marked as completed`);
507
509
  }
508
510
  catch (err) {
@@ -655,118 +657,6 @@ async function _ensureCleanWorkingTree() {
655
657
  ` - Leftover changes from previous session`);
656
658
  }
657
659
  }
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
660
  /**
771
661
  * Extract completed WU IDs from git log output.
772
662
  * @param {string} logOutput - Git log output (one commit per line)
@@ -1710,42 +1600,6 @@ async function executePreFlightChecks({ id, args, isBranchOnly, isDocsOnly, docM
1710
1600
  else {
1711
1601
  // Worktree mode: must be on main
1712
1602
  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
1603
  // Prevent coordination failures by ensuring main is up-to-date
1750
1604
  await ensureMainUpToDate();
1751
1605
  // P0 EMERGENCY FIX Part 1: Restore wu-events.jsonl BEFORE parallel completion check
@@ -1939,10 +1793,17 @@ async function executePreFlightChecks({ id, args, isBranchOnly, isDocsOnly, docM
1939
1793
  return { title, docForValidation };
1940
1794
  }
1941
1795
  async function executeGates({ id, args, isBranchOnly, isDocsOnly, worktreePath, branchName, }) {
1796
+ const gateResult = {
1797
+ fullGatesRanInCurrentRun: false,
1798
+ skippedByCheckpoint: false,
1799
+ checkpointId: null,
1800
+ };
1942
1801
  // WU-1747: Check if gates can be skipped based on valid checkpoint
1943
1802
  // This allows resuming wu:done without re-running gates if nothing changed
1944
1803
  const skipResult = canSkipGates(id, { currentHeadSha: undefined });
1945
1804
  if (skipResult.canSkip) {
1805
+ gateResult.skippedByCheckpoint = true;
1806
+ gateResult.checkpointId = skipResult.checkpoint.checkpointId;
1946
1807
  console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} ${CHECKPOINT_MESSAGES.SKIPPING_GATES_VALID}`);
1947
1808
  console.log(`${LOG_PREFIX.DONE} ${CHECKPOINT_MESSAGES.CHECKPOINT_LABEL}: ${skipResult.checkpoint.checkpointId}`);
1948
1809
  console.log(`${LOG_PREFIX.DONE} ${CHECKPOINT_MESSAGES.GATES_PASSED_AT}: ${skipResult.checkpoint.gatesPassedAt}`);
@@ -1954,7 +1815,7 @@ async function executeGates({ id, args, isBranchOnly, isDocsOnly, worktreePath,
1954
1815
  reason: SKIP_GATES_REASONS.CHECKPOINT_VALID,
1955
1816
  checkpoint_id: skipResult.checkpoint.checkpointId,
1956
1817
  });
1957
- return; // Skip gates entirely
1818
+ return gateResult; // Skip gates entirely
1958
1819
  }
1959
1820
  // WU-1747: Create checkpoint before gates for resumption on failure
1960
1821
  if (worktreePath && branchName) {
@@ -2071,6 +1932,7 @@ async function executeGates({ id, args, isBranchOnly, isDocsOnly, worktreePath,
2071
1932
  });
2072
1933
  die(`Gates failed in Branch-Only mode. Fix issues and try again.`);
2073
1934
  }
1935
+ gateResult.fullGatesRanInCurrentRun = true;
2074
1936
  }
2075
1937
  else if (worktreePath && existsSync(worktreePath)) {
2076
1938
  // Worktree mode: run gates in the dedicated worktree
@@ -2079,6 +1941,7 @@ async function executeGates({ id, args, isBranchOnly, isDocsOnly, worktreePath,
2079
1941
  isDocsOnly,
2080
1942
  docsOnly: Boolean(args.docsOnly),
2081
1943
  });
1944
+ gateResult.fullGatesRanInCurrentRun = true;
2082
1945
  }
2083
1946
  else {
2084
1947
  die(`Worktree not found (${worktreePath || 'unknown'}). Gates must run in the lane worktree.\n` +
@@ -2136,6 +1999,7 @@ async function executeGates({ id, args, isBranchOnly, isDocsOnly, worktreePath,
2136
1999
  // WU-1747: Mark checkpoint as gates passed for resumption on failure
2137
2000
  // This allows subsequent wu:done attempts to skip gates if nothing changed
2138
2001
  markGatesPassed(id);
2002
+ return gateResult;
2139
2003
  }
2140
2004
  /**
2141
2005
  * Print State HUD for visibility
@@ -2194,6 +2058,29 @@ async function main() {
2194
2058
  isBranchPR, derivedWorktree, docForValidation: initialDocForValidation, isDocsOnly, } = pathInfo;
2195
2059
  // Capture main checkout path once. process.cwd() may drift later during recovery flows.
2196
2060
  const mainCheckoutPath = process.cwd();
2061
+ // WU-1663: Determine prepPassed early for pipeline actor input.
2062
+ // canSkipGates checks if wu:prep already ran gates successfully via checkpoint.
2063
+ // This drives the isPrepPassed guard on the GATES_SKIPPED transition.
2064
+ const earlySkipResult = canSkipGates(id, { currentHeadSha: undefined });
2065
+ const prepPassed = earlySkipResult.canSkip;
2066
+ // WU-1663: Create XState pipeline actor for state-driven orchestration.
2067
+ // The actor tracks which pipeline stage we're in (validating, gating, committing, etc.)
2068
+ // and provides explicit state/transition contracts. Existing procedural logic continues
2069
+ // to do the real work; the actor provides structured state tracking alongside it.
2070
+ const pipelineActor = createActor(wuDoneMachine, {
2071
+ input: {
2072
+ wuId: id,
2073
+ worktreePath: derivedWorktree,
2074
+ prepPassed,
2075
+ },
2076
+ });
2077
+ pipelineActor.start();
2078
+ // WU-1663: Send START event to transition from idle -> validating
2079
+ pipelineActor.send({
2080
+ type: WU_DONE_EVENTS.START,
2081
+ wuId: id,
2082
+ worktreePath: derivedWorktree || '',
2083
+ });
2197
2084
  // WU-1590: branch-pr has no worktree, treat like branch-only for path resolution and ensureOnMain skip
2198
2085
  const isNoWorktreeMode = isBranchOnly || isBranchPR;
2199
2086
  const resolvedWorktreePath = derivedWorktree && !isNoWorktreeMode
@@ -2220,18 +2107,32 @@ async function main() {
2220
2107
  await ensureCleanWorktree(effectiveWorktreePath);
2221
2108
  }
2222
2109
  // 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
- });
2110
+ // WU-1663: Wrap in try/catch to send pipeline failure event before die() propagates
2111
+ let preFlightResult;
2112
+ try {
2113
+ preFlightResult = await executePreFlightChecks({
2114
+ id,
2115
+ args,
2116
+ isBranchOnly: effectiveBranchOnly,
2117
+ isDocsOnly,
2118
+ docMain,
2119
+ docForValidation: initialDocForValidation,
2120
+ derivedWorktree: effectiveDerivedWorktree,
2121
+ });
2122
+ }
2123
+ catch (preFlightErr) {
2124
+ pipelineActor.send({
2125
+ type: WU_DONE_EVENTS.VALIDATION_FAILED,
2126
+ error: getErrorMessage(preFlightErr),
2127
+ });
2128
+ pipelineActor.stop();
2129
+ throw preFlightErr;
2130
+ }
2232
2131
  const title = preFlightResult.title;
2233
2132
  // Note: docForValidation is returned but not used after pre-flight checks
2234
2133
  // The metadata transaction uses docForUpdate instead
2134
+ // WU-1663: Pre-flight checks passed - transition to preparing state
2135
+ pipelineActor.send({ type: WU_DONE_EVENTS.VALIDATION_PASSED });
2235
2136
  // WU-1599: Enforce auditable spawn provenance for initiative-governed WUs.
2236
2137
  await enforceSpawnProvenanceForDone(id, docMain, {
2237
2138
  baseDir: mainCheckoutPath,
@@ -2272,7 +2173,35 @@ async function main() {
2272
2173
  }
2273
2174
  // Otherwise silently allow - fail-open
2274
2175
  }
2275
- await executeGates({ id, args, isBranchOnly: effectiveBranchOnly, isDocsOnly, worktreePath });
2176
+ // WU-1663: Preparation complete - transition to gating state
2177
+ pipelineActor.send({ type: WU_DONE_EVENTS.PREPARATION_COMPLETE });
2178
+ // WU-1663: Wrap gates in try/catch to send pipeline failure event
2179
+ let gateExecutionResult;
2180
+ try {
2181
+ gateExecutionResult = await executeGates({
2182
+ id,
2183
+ args,
2184
+ isBranchOnly: effectiveBranchOnly,
2185
+ isDocsOnly,
2186
+ worktreePath,
2187
+ });
2188
+ }
2189
+ catch (gateErr) {
2190
+ pipelineActor.send({
2191
+ type: WU_DONE_EVENTS.GATES_FAILED,
2192
+ error: getErrorMessage(gateErr),
2193
+ });
2194
+ pipelineActor.stop();
2195
+ throw gateErr;
2196
+ }
2197
+ // WU-1663: Gates passed - transition from gating state.
2198
+ // Use GATES_SKIPPED if checkpoint dedup allowed skip, GATES_PASSED otherwise.
2199
+ if (gateExecutionResult.skippedByCheckpoint) {
2200
+ pipelineActor.send({ type: WU_DONE_EVENTS.GATES_SKIPPED });
2201
+ }
2202
+ else {
2203
+ pipelineActor.send({ type: WU_DONE_EVENTS.GATES_PASSED });
2204
+ }
2276
2205
  // Print State HUD for visibility (WU-1215: extracted to printStateHUD function)
2277
2206
  printStateHUD({
2278
2207
  id,
@@ -2282,12 +2211,17 @@ async function main() {
2282
2211
  derivedWorktree: effectiveDerivedWorktree,
2283
2212
  STAMPS_DIR,
2284
2213
  });
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) {
2214
+ // Step 0.5: Pre-flight hook validation policy.
2215
+ // WU-1659: Reuse Step 0 gate attestation/checkpoint and avoid duplicate full-suite execution.
2216
+ const preCommitGateDecision = resolveWuDonePreCommitGateDecision({
2217
+ skipGates: Boolean(args.skipGates),
2218
+ fullGatesRanInCurrentRun: gateExecutionResult.fullGatesRanInCurrentRun,
2219
+ skippedByCheckpoint: gateExecutionResult.skippedByCheckpoint,
2220
+ checkpointId: gateExecutionResult.checkpointId,
2221
+ });
2222
+ console.log(`${LOG_PREFIX.DONE} ${preCommitGateDecision.message}`);
2223
+ // Fallback path remains available if gate attestation is missing for any reason.
2224
+ if (preCommitGateDecision.runPreCommitFullSuite) {
2291
2225
  const hookResult = await validateAllPreCommitHooks(id, worktreePath, {
2292
2226
  runGates: ({ cwd }) => runGates({ cwd, docsOnly: false }),
2293
2227
  });
@@ -2295,9 +2229,6 @@ async function main() {
2295
2229
  die('Pre-flight validation failed. Fix hook issues and try again.');
2296
2230
  }
2297
2231
  }
2298
- else {
2299
- console.log(`${LOG_PREFIX.DONE} Skipping pre-flight hook validation (--skip-gates)`);
2300
- }
2301
2232
  // Step 0.6: WU-1781 - Run tasks:validate preflight BEFORE any merge/push operations
2302
2233
  // This prevents deadlocks where validation fails after merge, leaving local main ahead of origin
2303
2234
  // Specifically catches stamp-status mismatches from legacy WUs that would block pre-push hooks
@@ -2358,6 +2289,12 @@ async function main() {
2358
2289
  };
2359
2290
  completionResult = await executeWorktreeCompletion(worktreeContext);
2360
2291
  }
2292
+ // WU-1663: Mode-specific completion succeeded - send pipeline events.
2293
+ // The completion modules handle commit, merge, and push internally.
2294
+ // We send the corresponding pipeline events based on the completion result.
2295
+ pipelineActor.send({ type: WU_DONE_EVENTS.COMMIT_COMPLETE });
2296
+ pipelineActor.send({ type: WU_DONE_EVENTS.MERGE_COMPLETE });
2297
+ pipelineActor.send({ type: WU_DONE_EVENTS.PUSH_COMPLETE });
2361
2298
  // Handle recovery mode (zombie state cleanup completed)
2362
2299
  if ('recovered' in completionResult && completionResult.recovered) {
2363
2300
  // P0 FIX: Release lane lock before early exit
@@ -2369,30 +2306,28 @@ async function main() {
2369
2306
  catch {
2370
2307
  // Intentionally ignore lock release errors during cleanup
2371
2308
  }
2309
+ pipelineActor.stop();
2372
2310
  process.exit(EXIT_CODES.SUCCESS);
2373
2311
  }
2374
2312
  }
2375
2313
  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
- }
2314
+ // WU-1663: Mode execution failed - determine which stage failed
2315
+ // based on completion result flags and send appropriate failure event.
2316
+ const failureStage = completionResult.committed === false
2317
+ ? WU_DONE_EVENTS.COMMIT_FAILED
2318
+ : completionResult.merged === false
2319
+ ? WU_DONE_EVENTS.MERGE_FAILED
2320
+ : completionResult.pushed === false
2321
+ ? WU_DONE_EVENTS.PUSH_FAILED
2322
+ : WU_DONE_EVENTS.COMMIT_FAILED; // Default to commit as earliest possible failure
2323
+ pipelineActor.send({
2324
+ type: failureStage,
2325
+ error: getErrorMessage(err),
2326
+ });
2327
+ // WU-1663: Log pipeline state for diagnostics
2328
+ const failedSnapshot = pipelineActor.getSnapshot();
2329
+ console.error(`${LOG_PREFIX.DONE} Pipeline state: ${failedSnapshot.value} (failedAt: ${failedSnapshot.context.failedAt})`);
2330
+ pipelineActor.stop();
2396
2331
  // P0 FIX: Release lane lock before error exit
2397
2332
  try {
2398
2333
  const lane = docMain.lane;
@@ -2402,6 +2337,8 @@ async function main() {
2402
2337
  catch {
2403
2338
  // Intentionally ignore lock release errors during error handling
2404
2339
  }
2340
+ console.error(`\n${LOG_PREFIX.DONE} ${EMOJI.FAILURE} Mode execution failed: ${getErrorMessage(err)}`);
2341
+ console.error(`${LOG_PREFIX.DONE} ${EMOJI.INFO} Next step: resolve the reported error and retry: pnpm wu:done --id ${id}`);
2405
2342
  // WU-1811: Check if cleanup is safe before removing worktree
2406
2343
  // If cleanupSafe is false (or undefined), preserve worktree for recovery
2407
2344
  if (err.cleanupSafe === false) {
@@ -2415,58 +2352,6 @@ async function main() {
2415
2352
  else {
2416
2353
  await ensureNoAutoStagedOrNoop([WU_PATH, STATUS_PATH, BACKLOG_PATH, STAMPS_DIR]);
2417
2354
  }
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
2355
  // Step 6 & 7: Cleanup (remove worktree, delete branch) - WU-1215
2471
2356
  // WU-1811: Only run cleanup if all completion steps succeeded
2472
2357
  if (completionResult.cleanupSafe !== false) {
@@ -2539,6 +2424,12 @@ async function main() {
2539
2424
  // Double fail-open: even if runDecayOnDone itself throws unexpectedly, never block wu:done
2540
2425
  console.warn(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Decay archival error (fail-open): ${getErrorMessage(err)}`);
2541
2426
  }
2427
+ // WU-1663: Cleanup complete - transition to final done state
2428
+ pipelineActor.send({ type: WU_DONE_EVENTS.CLEANUP_COMPLETE });
2429
+ // WU-1663: Log final pipeline state for diagnostics
2430
+ const finalSnapshot = pipelineActor.getSnapshot();
2431
+ console.log(`${LOG_PREFIX.DONE} Pipeline state: ${finalSnapshot.value} (WU-1663)`);
2432
+ pipelineActor.stop();
2542
2433
  console.log(`\n${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Transaction COMMIT - all steps succeeded (WU-755)`);
2543
2434
  console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Marked done, pushed, and cleaned up.`);
2544
2435
  console.log(`- WU: ${id} — ${title}`);
@@ -2555,7 +2446,11 @@ async function main() {
2555
2446
  // WU-1983: Migration deployment nudge - only if supabase paths in code_paths
2556
2447
  const codePaths = docMain.code_paths || [];
2557
2448
  await printMigrationDeploymentNudge(codePaths, mainCheckoutPath);
2558
- if (allowLifecycleAutoCommit) {
2449
+ const currentBranch = (await getGitForCwd().getCurrentBranch()).trim();
2450
+ const shouldRunCleanupMutations = currentBranch.length > 0 &&
2451
+ currentBranch !== BRANCHES.MAIN &&
2452
+ currentBranch !== BRANCHES.MASTER;
2453
+ if (shouldRunCleanupMutations) {
2559
2454
  // WU-1366: Auto state cleanup after successful completion
2560
2455
  // Non-fatal: errors are logged but do not block completion
2561
2456
  await runAutoCleanupAfterDone(mainCheckoutPath);