@lumenflow/cli 3.17.6 → 3.18.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 (62) hide show
  1. package/dist/init-detection.js +5 -3
  2. package/dist/init-detection.js.map +1 -1
  3. package/dist/init-docs-scaffolder.js +96 -0
  4. package/dist/init-docs-scaffolder.js.map +1 -0
  5. package/dist/init-package-config.js +135 -0
  6. package/dist/init-package-config.js.map +1 -0
  7. package/dist/init-safety-scripts.js +129 -0
  8. package/dist/init-safety-scripts.js.map +1 -0
  9. package/dist/init-templates.js +4 -4
  10. package/dist/init-templates.js.map +1 -1
  11. package/dist/init.js +13 -302
  12. package/dist/init.js.map +1 -1
  13. package/dist/initiative-plan.js +1 -1
  14. package/dist/initiative-plan.js.map +1 -1
  15. package/dist/onboarding-template-paths.js +0 -1
  16. package/dist/onboarding-template-paths.js.map +1 -1
  17. package/dist/pre-commit-check.js +1 -1
  18. package/dist/pre-commit-check.js.map +1 -1
  19. package/dist/wu-done.js +389 -423
  20. package/dist/wu-done.js.map +1 -1
  21. package/dist/wu-edit-operations.js +4 -0
  22. package/dist/wu-edit-operations.js.map +1 -1
  23. package/dist/wu-edit-validators.js +4 -0
  24. package/dist/wu-edit-validators.js.map +1 -1
  25. package/dist/wu-edit.js +11 -0
  26. package/dist/wu-edit.js.map +1 -1
  27. package/dist/wu-spawn-strategy-resolver.js +13 -1
  28. package/dist/wu-spawn-strategy-resolver.js.map +1 -1
  29. package/package.json +8 -8
  30. package/packs/agent-runtime/.turbo/turbo-build.log +4 -0
  31. package/packs/agent-runtime/README.md +147 -0
  32. package/packs/agent-runtime/capability-factory.ts +104 -0
  33. package/packs/agent-runtime/config.schema.json +87 -0
  34. package/packs/agent-runtime/constants.ts +21 -0
  35. package/packs/agent-runtime/index.ts +11 -0
  36. package/packs/agent-runtime/manifest.ts +207 -0
  37. package/packs/agent-runtime/manifest.yaml +193 -0
  38. package/packs/agent-runtime/orchestration.ts +1787 -0
  39. package/packs/agent-runtime/pack-registration.ts +110 -0
  40. package/packs/agent-runtime/package.json +57 -0
  41. package/packs/agent-runtime/policy-factory.ts +165 -0
  42. package/packs/agent-runtime/tool-impl/agent-turn-tools.ts +793 -0
  43. package/packs/agent-runtime/tool-impl/index.ts +5 -0
  44. package/packs/agent-runtime/tool-impl/provider-adapters.ts +1245 -0
  45. package/packs/agent-runtime/tools/index.ts +4 -0
  46. package/packs/agent-runtime/tools/types.ts +47 -0
  47. package/packs/agent-runtime/tsconfig.json +20 -0
  48. package/packs/agent-runtime/types.ts +128 -0
  49. package/packs/agent-runtime/vitest.config.ts +11 -0
  50. package/packs/sidekick/.turbo/turbo-build.log +1 -1
  51. package/packs/sidekick/package.json +1 -1
  52. package/packs/sidekick/vitest.config.ts +11 -0
  53. package/packs/software-delivery/.turbo/turbo-build.log +1 -1
  54. package/packs/software-delivery/package.json +1 -1
  55. package/packs/software-delivery/vitest.config.ts +11 -0
  56. package/templates/core/.lumenflow/rules/wu-workflow.md.template +1 -1
  57. package/templates/core/LUMENFLOW.md.template +2 -2
  58. package/templates/core/ai/onboarding/first-wu-mistakes.md.template +2 -2
  59. package/templates/core/ai/onboarding/quick-ref-commands.md.template +1 -1
  60. package/templates/core/ai/onboarding/starting-prompt.md.template +1 -1
  61. package/templates/vendors/claude/.claude/skills/frontend-design/SKILL.md.template +1 -1
  62. package/templates/core/ai/onboarding/wu-sizing-guide.md.template +0 -84
package/dist/wu-done.js CHANGED
@@ -265,29 +265,9 @@ async function validateClaimMetadataBeforeGates(id, worktreePath, yamlStatus) {
265
265
  ` pnpm wu:done --id ${id}\n\n` +
266
266
  `See: https://lumenflow.dev/reference/troubleshooting-wu-done/ for more recovery options.`);
267
267
  }
268
- async function _assertWorktreeWUInProgressInStateStore(id, worktreePath) {
269
- const resolvedWorktreePath = path.resolve(worktreePath);
270
- const stateDir = resolveStateDir(resolvedWorktreePath);
271
- const eventsPath = path.join(resolvedWorktreePath, resolveWuEventsRelativePath(resolvedWorktreePath));
272
- const store = new WUStateStore(stateDir);
273
- try {
274
- await store.load();
275
- }
276
- catch (err) {
277
- die(`Cannot read WU state store for ${id}.\n\n` +
278
- `Path: ${eventsPath}\n\n` +
279
- `Error: ${getErrorMessage(err)}\n\n` +
280
- `If this WU was claimed on an older tool version or the event log is missing/corrupt,\n` +
281
- `repair the worktree state store before rerunning wu:done.`);
282
- }
283
- const inProgress = store.getByStatus(WU_STATUS.IN_PROGRESS);
284
- if (!inProgress.has(id)) {
285
- die(`WU ${id} is not in_progress in the worktree state store.\n\n` +
286
- `Path: ${eventsPath}\n\n` +
287
- `This will fail later when wu:done tries to append a complete event and regenerate backlog/status.\n` +
288
- `Fix the claim/state log first, then rerun wu:done.`);
289
- }
290
- }
268
+ // _assertWorktreeWUInProgressInStateStore removed (WU-2400): dead code,
269
+ // strict subset of validateClaimMetadataBeforeGates which already covers
270
+ // YAML status + state store checks with actionable repair guidance.
291
271
  /**
292
272
  * WU-1946: Update spawn registry on WU completion.
293
273
  * Non-blocking wrapper - failures logged as warnings.
@@ -354,9 +334,13 @@ function getCommitHeaderLimit() {
354
334
  }
355
335
  }
356
336
  // ensureOnMain() moved to wu-helpers.ts (WU-1256)
357
- async function auditSkipGates(id, reason, fixWU, worktreePath) {
337
+ /**
338
+ * WU-2400: Generic audit entry appender — shared by auditSkipGates and auditSkipCosGates.
339
+ * Consolidates the duplicate git-identity lookup, JSON serialisation, and file-append logic.
340
+ */
341
+ async function appendAuditEntry(filename, buildEntry, worktreePath, options) {
358
342
  const auditBaseDir = worktreePath || process.cwd();
359
- const auditPath = path.join(auditBaseDir, '.lumenflow', SKIP_GATES_AUDIT_FILENAME);
343
+ const auditPath = path.join(auditBaseDir, '.lumenflow', filename);
360
344
  const auditDir = path.dirname(auditPath);
361
345
  if (!existsSync(auditDir))
362
346
  mkdirSync(auditDir, { recursive: true });
@@ -364,18 +348,26 @@ async function auditSkipGates(id, reason, fixWU, worktreePath) {
364
348
  const userName = await gitAdapter.getConfigValue(GIT_CONFIG_USER_NAME);
365
349
  const userEmail = await gitAdapter.getConfigValue(GIT_CONFIG_USER_EMAIL);
366
350
  const commitHash = await gitAdapter.getCommitHash();
367
- const entry = buildSkipGatesAuditEntry({
368
- id,
369
- reason,
370
- fixWU,
371
- worktreePath,
351
+ const entry = buildEntry({
372
352
  userName: userName.trim(),
373
353
  userEmail: userEmail.trim(),
374
354
  commitHash: commitHash.trim(),
375
355
  });
376
356
  const line = JSON.stringify(entry);
377
357
  appendFileSync(auditPath, `${line}\n`, { encoding: FILE_SYSTEM.UTF8 });
378
- console.log(`${LOG_PREFIX.DONE} ${EMOJI.MEMO} Skip-gates event logged to ${path.relative(process.cwd(), auditPath) || auditPath}`);
358
+ const label = options?.logLabel ?? `Audit event logged to`;
359
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.MEMO} ${label} ${path.relative(process.cwd(), auditPath) || auditPath}`);
360
+ }
361
+ async function auditSkipGates(id, reason, fixWU, worktreePath) {
362
+ await appendAuditEntry(SKIP_GATES_AUDIT_FILENAME, (git) => buildSkipGatesAuditEntry({
363
+ id,
364
+ reason,
365
+ fixWU,
366
+ worktreePath,
367
+ userName: git.userName,
368
+ userEmail: git.userEmail,
369
+ commitHash: git.commitHash,
370
+ }), worktreePath, { logLabel: 'Skip-gates event logged to' });
379
371
  }
380
372
  export function buildSkipGatesAuditEntry(input) {
381
373
  const reasonText = typeof input.reason === 'string' ? input.reason : undefined;
@@ -394,28 +386,17 @@ export function buildSkipGatesAuditEntry(input) {
394
386
  /**
395
387
  * Audit trail for COS gates skip (COS v1.3 S7)
396
388
  * WU-1852: Renamed from skip-cos-gates to avoid referencing non-existent CLI flag
389
+ * WU-2400: Delegates to appendAuditEntry to remove DRY violation
397
390
  */
398
391
  async function auditSkipCosGates(id, reason, worktreePath) {
399
- const auditBaseDir = worktreePath || process.cwd();
400
- const auditPath = path.join(auditBaseDir, '.lumenflow', 'skip-cos-gates-audit.log');
401
- const auditDir = path.dirname(auditPath);
402
- if (!existsSync(auditDir))
403
- mkdirSync(auditDir, { recursive: true });
404
- const gitAdapter = getGitForCwd();
405
- const userName = await gitAdapter.getConfigValue(GIT_CONFIG_USER_NAME);
406
- const userEmail = await gitAdapter.getConfigValue(GIT_CONFIG_USER_EMAIL);
407
- const commitHash = await gitAdapter.getCommitHash();
408
392
  const reasonText = typeof reason === 'string' ? reason : undefined;
409
- const entry = {
393
+ await appendAuditEntry('skip-cos-gates-audit.log', (git) => ({
410
394
  timestamp: new Date().toISOString(),
411
395
  wu_id: id,
412
396
  reason: reasonText || DEFAULT_NO_REASON,
413
- git_user: `${userName.trim()} <${userEmail.trim()}>`,
414
- git_commit: commitHash.trim(),
415
- };
416
- const line = JSON.stringify(entry);
417
- appendFileSync(auditPath, `${line}\n`, { encoding: FILE_SYSTEM.UTF8 });
418
- console.log(`${LOG_PREFIX.DONE} ${EMOJI.MEMO} Skip-COS-gates event logged to ${auditPath}`);
397
+ git_user: `${git.userName} <${git.userEmail}>`,
398
+ git_commit: git.commitHash,
399
+ }), worktreePath, { logLabel: 'Skip-COS-gates event logged to' });
419
400
  }
420
401
  // WU-2308: validateAllPreCommitHooks moved to wu-done-validators.ts
421
402
  // Now accepts worktreePath parameter to run audit from worktree context
@@ -470,20 +451,10 @@ function recordTransactionState(id, wuPath, stampPath, backlogPath, statusPath)
470
451
  * @param {string} backlogPath - Path to backlog.md (WU-1230)
471
452
  * @param {string} statusPath - Path to status.md (WU-1230)
472
453
  */
473
- async function rollbackTransaction(txState, wuPath, stampPath, backlogPath, statusPath) {
474
- console.error(`\n${LOG_PREFIX.DONE} ${EMOJI.WARNING} ROLLING BACK TRANSACTION (WU-755 + WU-1230 + WU-1255 + WU-1280)...`);
475
- // WU-1280: ATOMIC ROLLBACK - Clean git state FIRST, then restore files
476
- // Previous order (restore → git checkout) caused issues:
477
- // - git checkout -- . would UNDO file restorations
478
- // - Left messy state with staged + unstaged conflicts
479
- //
480
- // New order:
481
- // 1. Unstage everything (git reset HEAD)
482
- // 2. Discard working tree changes (git checkout -- .)
483
- // 3. Remove stamp if created
484
- // 4. THEN restore files from txState
454
+ // ── WU-2400: Named rollback sub-operations extracted from rollbackTransaction ──
455
+ /** Unstage all staged files and discard working tree changes. */
456
+ async function resetGitStaging() {
485
457
  // Step 1: Unstage all staged files FIRST
486
- // Emergency fix Session 2: Use git-adapter instead of raw execSync
487
458
  try {
488
459
  const gitAdapter = getGitForCwd();
489
460
  await gitAdapter.raw(['reset', 'HEAD']);
@@ -493,7 +464,6 @@ async function rollbackTransaction(txState, wuPath, stampPath, backlogPath, stat
493
464
  // Ignore - may not have anything staged
494
465
  }
495
466
  // Step 2: Discard working directory changes (reset to last commit)
496
- // Emergency fix Session 2: Use git-adapter instead of raw execSync
497
467
  try {
498
468
  const gitAdapter = getGitForCwd();
499
469
  await gitAdapter.raw(['checkout', '--', '.']);
@@ -502,43 +472,42 @@ async function rollbackTransaction(txState, wuPath, stampPath, backlogPath, stat
502
472
  catch {
503
473
  // Ignore - may not have anything to discard
504
474
  }
505
- // Step 3: Remove stamp unconditionally if it exists (WU-1440)
506
- // Previous behavior only removed if !stampExisted, but that flag could be wrong
507
- // due to edge cases. Unconditional removal ensures clean rollback state.
508
- if (existsSync(stampPath)) {
509
- try {
510
- unlinkSync(stampPath);
511
- console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Removed ${stampPath}`);
512
- }
513
- catch (err) {
514
- console.error(`${LOG_PREFIX.DONE} ${EMOJI.FAILURE} Failed to remove stamp: ${getErrorMessage(err)}`);
515
- }
475
+ }
476
+ /** Remove stamp file unconditionally if it exists (WU-1440). */
477
+ function removeStampIfExists(stampPath) {
478
+ if (!existsSync(stampPath))
479
+ return;
480
+ try {
481
+ unlinkSync(stampPath);
482
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Removed ${stampPath}`);
483
+ }
484
+ catch (err) {
485
+ console.error(`${LOG_PREFIX.DONE} ${EMOJI.FAILURE} Failed to remove stamp: ${getErrorMessage(err)}`);
516
486
  }
517
- // Step 4: Restore files from txState (AFTER git cleanup)
518
- // Build list of files to restore with per-file error tracking (ref: WU-1255)
487
+ }
488
+ /** Build file list and restore from transaction snapshot (WU-1255). */
489
+ function restoreFilesFromSnapshot(txState, wuPath, backlogPath, statusPath) {
519
490
  const filesToRestore = [];
520
- // Restore backlog.md (ref: WU-1230)
521
491
  if (txState.backlogContent && existsSync(backlogPath)) {
522
492
  filesToRestore.push({ name: 'backlog.md', path: backlogPath, content: txState.backlogContent });
523
493
  }
524
- // Restore status.md (ref: WU-1230)
525
494
  if (txState.statusContent && existsSync(statusPath)) {
526
495
  filesToRestore.push({ name: 'status.md', path: statusPath, content: txState.statusContent });
527
496
  }
528
- // Restore WU YAML if it was modified
529
497
  if (txState.wuYamlContent && existsSync(wuPath)) {
530
498
  filesToRestore.push({ name: 'WU YAML', path: wuPath, content: txState.wuYamlContent });
531
499
  }
532
- // WU-1255: Use rollbackFiles utility for per-file error tracking
533
500
  const restoreResult = rollbackFiles(filesToRestore);
534
- // Log results
535
501
  for (const name of restoreResult.restored) {
536
502
  console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Restored ${name}`);
537
503
  }
538
504
  for (const err of restoreResult.errors) {
539
505
  console.error(`${LOG_PREFIX.DONE} ${EMOJI.FAILURE} Failed to restore ${err.name}: ${err.error}`);
540
506
  }
541
- // Reset main to original SHA if we're on main
507
+ return restoreResult;
508
+ }
509
+ /** Reset main branch to original SHA if we drifted during the transaction. */
510
+ async function resetMainBranchIfNeeded(txState) {
542
511
  try {
543
512
  const gitAdapter = getGitForCwd();
544
513
  const currentBranch = await gitAdapter.getCurrentBranch();
@@ -546,7 +515,6 @@ async function rollbackTransaction(txState, wuPath, stampPath, backlogPath, stat
546
515
  const currentSHA = await gitAdapter.getCommitHash();
547
516
  if (currentSHA !== txState.mainSHA) {
548
517
  await gitAdapter.reset(txState.mainSHA, { hard: true });
549
- // Emergency fix Session 2: Use GIT.SHA_SHORT_LENGTH constant
550
518
  console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Reset main to ${txState.mainSHA.slice(0, GIT.SHA_SHORT_LENGTH)}`);
551
519
  }
552
520
  }
@@ -554,9 +522,9 @@ async function rollbackTransaction(txState, wuPath, stampPath, backlogPath, stat
554
522
  catch (e) {
555
523
  console.warn(`${LOG_PREFIX.DONE} Warning: Could not reset main: ${getErrorMessage(e)}`);
556
524
  }
557
- // WU-1280: Verify clean git status after rollback
558
- // WU-1281: Extracted to helper to fix repeated parsing and magic number
559
- // Emergency fix Session 2: Use git-adapter instead of raw execSync
525
+ }
526
+ /** WU-1280: Verify clean git status after rollback and log any residue. */
527
+ async function verifyCleanGitStateAfterRollback() {
560
528
  try {
561
529
  const gitAdapter = getGitForCwd();
562
530
  const statusOutput = (await gitAdapter.raw(['status', '--porcelain'])).trim();
@@ -570,6 +538,17 @@ async function rollbackTransaction(txState, wuPath, stampPath, backlogPath, stat
570
538
  catch {
571
539
  // Ignore - git status may fail in edge cases
572
540
  }
541
+ }
542
+ // ── End of extracted rollback sub-operations ──
543
+ async function rollbackTransaction(txState, wuPath, stampPath, backlogPath, statusPath) {
544
+ console.error(`\n${LOG_PREFIX.DONE} ${EMOJI.WARNING} ROLLING BACK TRANSACTION (WU-755 + WU-1230 + WU-1255 + WU-1280)...`);
545
+ // WU-1280: ATOMIC ROLLBACK - Clean git state FIRST, then restore files
546
+ // WU-2400: Delegates to named sub-operations for readability.
547
+ await resetGitStaging();
548
+ removeStampIfExists(stampPath);
549
+ const restoreResult = restoreFilesFromSnapshot(txState, wuPath, backlogPath, statusPath);
550
+ await resetMainBranchIfNeeded(txState);
551
+ await verifyCleanGitStateAfterRollback();
573
552
  // WU-1255: Report final status with all errors
574
553
  if (restoreResult.errors.length > 0) {
575
554
  console.error(`\n${LOG_PREFIX.DONE} ${EMOJI.WARNING} Rollback completed with ${restoreResult.errors.length} error(s):`);
@@ -650,9 +629,9 @@ function runWUValidator(doc, id, allowTodo = false, worktreePath = null) {
650
629
  * @param {string|null} params.derivedWorktree - Derived worktree path
651
630
  * @returns {Promise<{title: string, docForValidation: object}>} Updated title and doc
652
631
  */
653
- // eslint-disable-next-line sonarjs/cognitive-complexity -- Pre-existing complexity, refactor tracked separately
654
- async function executePreFlightChecks({ id, args, isBranchOnly, isDocsOnly, docMain, docForValidation, derivedWorktree, }) {
655
- // YAML schema validation
632
+ // ── WU-2400: Named validator functions extracted from executePreFlightChecks ──
633
+ /** Validate WU YAML against Zod schema and done-specific rules. */
634
+ function preflightValidateYamlSchema(docForValidation) {
656
635
  console.log(`${LOG_PREFIX.DONE} Validating WU YAML structure...`);
657
636
  const schemaResult = validateWU(docForValidation);
658
637
  if (!schemaResult.success) {
@@ -661,7 +640,6 @@ async function executePreFlightChecks({ id, args, isBranchOnly, isDocsOnly, docM
661
640
  .join(STRING_LITERALS.NEWLINE);
662
641
  die(`❌ WU YAML validation failed:\n\n${errors}\n\nFix these issues before running wu:done`);
663
642
  }
664
- // Additional done-specific validation
665
643
  if (docForValidation.status === WU_STATUS.DONE) {
666
644
  const doneResult = validateDoneWU(schemaResult.data);
667
645
  if (!doneResult.valid) {
@@ -669,10 +647,12 @@ async function executePreFlightChecks({ id, args, isBranchOnly, isDocsOnly, docM
669
647
  }
670
648
  }
671
649
  console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} WU YAML validation passed`);
672
- // WU-2079: Approval gate validation
673
- // Ensures required approvals are present before allowing completion
650
+ return schemaResult;
651
+ }
652
+ /** WU-2079: Ensure required approvals are present before allowing completion. */
653
+ function preflightValidateApprovalGates(id, schemaData) {
674
654
  console.log(`${LOG_PREFIX.DONE} Checking approval gates...`);
675
- const approvalResult = validateApprovalGates(schemaResult.data);
655
+ const approvalResult = validateApprovalGates(schemaData);
676
656
  if (!approvalResult.valid) {
677
657
  const governancePath = getConfig({ projectRoot: process.cwd() }).directories.governancePath;
678
658
  die(`❌ Approval gates not satisfied:\n\n${approvalResult.errors.map((e) => ` - ${e}`).join(STRING_LITERALS.NEWLINE)}\n\n` +
@@ -682,15 +662,16 @@ async function executePreFlightChecks({ id, args, isBranchOnly, isDocsOnly, docM
682
662
  ` 3. Re-run: pnpm wu:done --id ${id}\n\n` +
683
663
  ` See ${governancePath} for role definitions.`);
684
664
  }
685
- // Log advisory warnings (non-blocking)
686
665
  if (approvalResult.warnings.length > 0) {
687
666
  approvalResult.warnings.forEach((w) => {
688
667
  console.warn(`${LOG_PREFIX.DONE} ⚠️ ${w}`);
689
668
  });
690
669
  }
691
670
  console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Approval gates passed`);
671
+ }
672
+ /** WU-1805 + WU-2310: Validate code_paths consistency (preflight + type vs code_paths). */
673
+ async function preflightValidateCodePathsConsistency(id, docForValidation, derivedWorktree) {
692
674
  // WU-1805: Preflight code_paths and test_paths validation
693
- // Run BEFORE gates to catch YAML mismatches early (saves time vs. discovering after full gate run)
694
675
  const preflightResult = await executePreflightCodePathValidation(id, {
695
676
  rootDir: process.cwd(),
696
677
  worktreePath: derivedWorktree,
@@ -706,7 +687,6 @@ async function executePreFlightChecks({ id, args, isBranchOnly, isDocsOnly, docM
706
687
  }
707
688
  }
708
689
  // WU-2310: Preflight type vs code_paths validation
709
- // Run BEFORE transaction to prevent documentation WUs with code paths from failing at git commit
710
690
  console.log(`${LOG_PREFIX.DONE} Validating type vs code_paths (WU-2310)...`);
711
691
  const typeVsCodePathsResult = validateTypeVsCodePathsPreflight(docForValidation);
712
692
  if (!typeVsCodePathsResult.valid) {
@@ -714,10 +694,9 @@ async function executePreFlightChecks({ id, args, isBranchOnly, isDocsOnly, docM
714
694
  die(errorMessage);
715
695
  }
716
696
  console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Type vs code_paths validation passed`);
717
- // Tripwire: Scan commands log for violations
718
- runTripwireCheck();
719
- // WU-1234: Pre-flight backlog consistency check
720
- // Fail fast if WU is in both Done and In Progress sections
697
+ }
698
+ /** WU-1234 + WU-1276: Validate backlog and WU state store consistency. */
699
+ async function preflightValidateBacklogAndStateConsistency(id) {
721
700
  console.log(`${LOG_PREFIX.DONE} Checking backlog consistency...`);
722
701
  const backlogPath = WU_PATHS.BACKLOG();
723
702
  const backlogConsistency = checkBacklogConsistencyForWU(id, backlogPath);
@@ -725,8 +704,6 @@ async function executePreFlightChecks({ id, args, isBranchOnly, isDocsOnly, docM
725
704
  die(backlogConsistency.error ?? 'Backlog consistency check failed');
726
705
  }
727
706
  console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Backlog consistency check passed`);
728
- // WU-1276: Pre-flight WU state consistency check
729
- // Layer 2 defense-in-depth: fail fast if WU has pre-existing inconsistencies
730
707
  console.log(`${LOG_PREFIX.DONE} Checking WU state consistency...`);
731
708
  const stateCheck = await checkWUConsistency(id);
732
709
  if (!stateCheck.valid) {
@@ -737,7 +714,10 @@ async function executePreFlightChecks({ id, args, isBranchOnly, isDocsOnly, docM
737
714
  `Fix with: pnpm wu:repair --id ${id}`);
738
715
  }
739
716
  console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} WU state consistency check passed`);
740
- // Branch-Only mode validation
717
+ }
718
+ /** Validate worktree state: branch-only vs worktree mode, parallel detection, claim metadata. */
719
+ async function preflightValidateWorktreeState(params) {
720
+ const { id, args, isBranchOnly, docMain, docForValidation, derivedWorktree } = params;
741
721
  if (isBranchOnly) {
742
722
  const laneBranch = await defaultBranchFrom(docMain);
743
723
  if (!laneBranch)
@@ -748,193 +728,162 @@ async function executePreFlightChecks({ id, args, isBranchOnly, isDocsOnly, docM
748
728
  }
749
729
  console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Branch-Only mode validation passed`);
750
730
  console.log(`${LOG_PREFIX.DONE} Working on branch: ${laneBranch}`);
731
+ return;
751
732
  }
752
- else {
753
- // Worktree mode: must be on main
754
- await ensureOnMain(getGitForCwd());
755
- // P0 EMERGENCY FIX Part 1: Restore wu-events.jsonl BEFORE parallel completion check
756
- // Previous wu:done runs or memory layer writes may have left this file dirty,
757
- // which causes the auto-rebase to fail with "You have unstaged changes"
758
- // WU-2370: Preserve wu:brief evidence lines across the restore. Without this,
759
- // a failed wu:done retry loses uncommitted evidence written by wu:brief, causing
760
- // "Missing wu:brief evidence" errors on subsequent attempts.
761
- if (derivedWorktree) {
762
- const wuEventsRelPath = resolveWuEventsRelativePath(derivedWorktree);
763
- const wuEventsAbsPath = path.join(derivedWorktree, wuEventsRelPath);
764
- // Read uncommitted wu:brief evidence lines before restore
765
- let uncommittedBriefLines = [];
766
- try {
767
- const preRestoreContent = readFileSync(wuEventsAbsPath, {
768
- encoding: FILE_SYSTEM.UTF8,
769
- });
770
- const committedContent = (() => {
771
- try {
772
- return execSync(`git -C "${derivedWorktree}" show HEAD:"${wuEventsRelPath}"`, {
773
- encoding: FILE_SYSTEM.UTF8,
774
- });
775
- }
776
- catch {
777
- return '';
778
- }
779
- })();
780
- const committedLines = new Set(committedContent.trim().split('\n').filter(Boolean));
781
- uncommittedBriefLines = preRestoreContent
782
- .trim()
783
- .split('\n')
784
- .filter((line) => !committedLines.has(line) && line.includes('[wu:brief]'));
785
- }
786
- catch {
787
- // Non-fatal: file might not exist
788
- }
789
- try {
790
- execSync(`git -C "${derivedWorktree}" restore "${wuEventsRelPath}"`);
791
- }
792
- catch {
793
- // Non-fatal: file might not exist or already clean
794
- }
795
- // Re-append preserved wu:brief evidence lines
796
- if (uncommittedBriefLines.length > 0) {
733
+ // Worktree mode: must be on main
734
+ await ensureOnMain(getGitForCwd());
735
+ // P0 EMERGENCY FIX Part 1: Restore wu-events.jsonl BEFORE parallel completion check
736
+ // WU-2370: Preserve wu:brief evidence lines across the restore.
737
+ if (derivedWorktree) {
738
+ const wuEventsRelPath = resolveWuEventsRelativePath(derivedWorktree);
739
+ const wuEventsAbsPath = path.join(derivedWorktree, wuEventsRelPath);
740
+ let uncommittedBriefLines = [];
741
+ try {
742
+ const preRestoreContent = readFileSync(wuEventsAbsPath, {
743
+ encoding: FILE_SYSTEM.UTF8,
744
+ });
745
+ const committedContent = (() => {
797
746
  try {
798
- appendFileSync(wuEventsAbsPath, uncommittedBriefLines.join('\n') + '\n', {
747
+ return execSync(`git -C "${derivedWorktree}" show HEAD:"${wuEventsRelPath}"`, {
799
748
  encoding: FILE_SYSTEM.UTF8,
800
749
  });
801
750
  }
802
751
  catch {
803
- // Non-fatal: best-effort evidence preservation
752
+ return '';
804
753
  }
805
- }
754
+ })();
755
+ const committedLines = new Set(committedContent.trim().split('\n').filter(Boolean));
756
+ uncommittedBriefLines = preRestoreContent
757
+ .trim()
758
+ .split('\n')
759
+ .filter((line) => !committedLines.has(line) && line.includes('[wu:brief]'));
806
760
  }
807
- // WU-1382: Detect parallel completions and warn
808
- // WU-1584 Fix #3: Trigger auto-rebase instead of just warning
809
- console.log(`${LOG_PREFIX.DONE} Checking for parallel WU completions...`);
810
- const parallelResult = await detectParallelCompletions(id, docForValidation);
811
- if (parallelResult.hasParallelCompletions) {
812
- console.warn(parallelResult.warning);
813
- // Emit telemetry for parallel detection
814
- emitTelemetry({
815
- script: 'wu-done',
816
- wu_id: id,
817
- step: 'parallel_detection',
818
- parallel_wus: parallelResult.completedWUs,
819
- count: parallelResult.completedWUs.length,
820
- });
821
- // WU-1588: Check inbox for recent signals from parallel agents
822
- // Non-blocking: failures handled internally by checkInboxForRecentSignals
823
- await checkInboxForRecentSignals(id);
824
- // WU-1584: Instead of proceeding with warning, trigger auto-rebase
825
- // This prevents merge conflicts that would fail downstream
826
- if (derivedWorktree && !args.noAutoRebase) {
827
- console.log(`${LOG_PREFIX.DONE} ${EMOJI.INFO} WU-1584: Triggering auto-rebase to incorporate parallel completions...`);
828
- const laneBranch = await defaultBranchFrom(docForValidation);
829
- if (laneBranch) {
830
- const rebaseResult = await autoRebaseBranch(laneBranch, derivedWorktree, id);
831
- if (rebaseResult.success) {
832
- console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} WU-1584: Auto-rebase complete - parallel completions incorporated`);
833
- emitTelemetry({
834
- script: MICRO_WORKTREE_OPERATIONS.WU_DONE,
835
- wu_id: id,
836
- step: TELEMETRY_STEPS.PARALLEL_AUTO_REBASE,
837
- parallel_wus: parallelResult.completedWUs,
838
- count: parallelResult.completedWUs.length,
839
- });
840
- }
841
- else {
842
- // Rebase failed - provide detailed instructions
843
- console.error(`${LOG_PREFIX.DONE} ${EMOJI.FAILURE} Auto-rebase failed`);
844
- console.error(rebaseResult.error);
845
- die(`WU-1584: Auto-rebase failed after detecting parallel completions.\n` +
846
- `Manual resolution required - see instructions above.`);
847
- }
848
- }
761
+ catch {
762
+ // Non-fatal: file might not exist
763
+ }
764
+ try {
765
+ execSync(`git -C "${derivedWorktree}" restore "${wuEventsRelPath}"`);
766
+ }
767
+ catch {
768
+ // Non-fatal: file might not exist or already clean
769
+ }
770
+ if (uncommittedBriefLines.length > 0) {
771
+ try {
772
+ appendFileSync(wuEventsAbsPath, uncommittedBriefLines.join('\n') + '\n', {
773
+ encoding: FILE_SYSTEM.UTF8,
774
+ });
849
775
  }
850
- else if (!args.noAutoRebase) {
851
- // No worktree path available - warn and proceed (legacy behavior)
852
- console.log(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Cannot auto-rebase (no worktree path) - proceeding with caution`);
776
+ catch {
777
+ // Non-fatal: best-effort evidence preservation
853
778
  }
854
- else {
855
- // Auto-rebase disabled - warn and proceed
856
- console.log(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Auto-rebase disabled (--no-auto-rebase) - proceeding with caution`);
779
+ }
780
+ }
781
+ // WU-1382: Detect parallel completions and warn
782
+ // WU-1584 Fix #3: Trigger auto-rebase instead of just warning
783
+ console.log(`${LOG_PREFIX.DONE} Checking for parallel WU completions...`);
784
+ const parallelResult = await detectParallelCompletions(id, docForValidation);
785
+ if (parallelResult.hasParallelCompletions) {
786
+ console.warn(parallelResult.warning);
787
+ emitTelemetry({
788
+ script: 'wu-done',
789
+ wu_id: id,
790
+ step: 'parallel_detection',
791
+ parallel_wus: parallelResult.completedWUs,
792
+ count: parallelResult.completedWUs.length,
793
+ });
794
+ await checkInboxForRecentSignals(id);
795
+ if (derivedWorktree && !args.noAutoRebase) {
796
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.INFO} WU-1584: Triggering auto-rebase to incorporate parallel completions...`);
797
+ const laneBranch = await defaultBranchFrom(docForValidation);
798
+ if (laneBranch) {
799
+ const rebaseResult = await autoRebaseBranch(laneBranch, derivedWorktree, id);
800
+ if (rebaseResult.success) {
801
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} WU-1584: Auto-rebase complete - parallel completions incorporated`);
802
+ emitTelemetry({
803
+ script: MICRO_WORKTREE_OPERATIONS.WU_DONE,
804
+ wu_id: id,
805
+ step: TELEMETRY_STEPS.PARALLEL_AUTO_REBASE,
806
+ parallel_wus: parallelResult.completedWUs,
807
+ count: parallelResult.completedWUs.length,
808
+ });
809
+ }
810
+ else {
811
+ console.error(`${LOG_PREFIX.DONE} ${EMOJI.FAILURE} Auto-rebase failed`);
812
+ console.error(rebaseResult.error);
813
+ die(`WU-1584: Auto-rebase failed after detecting parallel completions.\n` +
814
+ `Manual resolution required - see instructions above.`);
815
+ }
857
816
  }
858
817
  }
859
- // WU-1381: Detect background processes that might interfere with gates
860
- // Non-blocking warning - helps agents understand mixed stdout/stderr output
861
- if (derivedWorktree) {
862
- await runBackgroundProcessCheck(derivedWorktree);
818
+ else if (!args.noAutoRebase) {
819
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Cannot auto-rebase (no worktree path) - proceeding with caution`);
863
820
  }
864
- // WU-1804: Fail fast before gates with comprehensive claim metadata check.
865
- // Validates both YAML status AND state store BEFORE gates, not just one of them.
866
- // Provides actionable guidance to run wu:repair --claim if validation fails.
867
- if (derivedWorktree) {
868
- await validateClaimMetadataBeforeGates(id, derivedWorktree, docForValidation.status);
821
+ else {
822
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Auto-rebase disabled (--no-auto-rebase) - proceeding with caution`);
869
823
  }
870
824
  }
871
- // Use worktree title for commit message (not stale main title)
872
- const title = docForValidation.title || docMain.title || '';
873
- if (isDocsOnly) {
874
- console.log('\n📝 Docs-only WU detected');
875
- console.log(' - Gates will skip lint/typecheck/tests');
876
- console.log(' - Only docs/markdown paths allowed\n');
825
+ // WU-1381: Detect background processes that might interfere with gates
826
+ if (derivedWorktree) {
827
+ await runBackgroundProcessCheck(derivedWorktree);
877
828
  }
878
- if (isBranchOnly) {
879
- console.log('\n🌿 Branch-Only mode detected');
880
- console.log(' - Gates run in main checkout on lane branch');
881
- console.log(' - No worktree to remove\n');
829
+ // WU-1804: Fail fast before gates with comprehensive claim metadata check.
830
+ if (derivedWorktree) {
831
+ await validateClaimMetadataBeforeGates(id, derivedWorktree, docForValidation.status);
832
+ }
833
+ }
834
+ /** Validate ownership: session ownership + assigned_to ownership (worktree mode only). */
835
+ async function preflightValidateOwnership(params) {
836
+ const { id, args, isBranchOnly, docForValidation, derivedWorktree } = params;
837
+ if (isBranchOnly)
838
+ return;
839
+ const activeSession = getCurrentSessionForWU();
840
+ const prepCheckpointResult = await resolveCheckpointSkipResult(id, derivedWorktree || null);
841
+ const sessionOwnership = validateClaimSessionOwnership({
842
+ wuId: id,
843
+ claimedSessionId: typeof docForValidation.session_id === 'string' ? docForValidation.session_id : null,
844
+ activeSessionId: activeSession?.session_id ?? null,
845
+ force: Boolean(args.force),
846
+ hasValidPrepCheckpoint: prepCheckpointResult.canSkip,
847
+ skipGates: Boolean(args['skip-gates']),
848
+ });
849
+ if (!sessionOwnership.valid) {
850
+ die(sessionOwnership.error ?? 'Claim-session ownership check failed');
882
851
  }
883
- // Ownership check (skip in branch-only mode)
884
- if (!isBranchOnly) {
885
- const activeSession = getCurrentSessionForWU();
886
- // WU-2341: Check if wu:prep created a valid checkpoint — proves authorized session handoff.
887
- const prepCheckpointResult = await resolveCheckpointSkipResult(id, derivedWorktree || null);
888
- const sessionOwnership = validateClaimSessionOwnership({
852
+ if (sessionOwnership.auditRequired &&
853
+ typeof docForValidation.session_id === 'string' &&
854
+ derivedWorktree) {
855
+ await appendClaimSessionOverrideAuditEvent({
889
856
  wuId: id,
890
- claimedSessionId: typeof docForValidation.session_id === 'string' ? docForValidation.session_id : null,
857
+ claimedSessionId: docForValidation.session_id,
891
858
  activeSessionId: activeSession?.session_id ?? null,
892
- force: Boolean(args.force),
893
- hasValidPrepCheckpoint: prepCheckpointResult.canSkip,
894
- skipGates: Boolean(args['skip-gates']),
859
+ reason: args.reason || 'force ownership override',
860
+ worktreePath: derivedWorktree,
895
861
  });
896
- if (!sessionOwnership.valid) {
897
- die(sessionOwnership.error ?? 'Claim-session ownership check failed');
898
- }
899
- if (sessionOwnership.auditRequired &&
900
- typeof docForValidation.session_id === 'string' &&
901
- derivedWorktree) {
902
- await appendClaimSessionOverrideAuditEvent({
903
- wuId: id,
904
- claimedSessionId: docForValidation.session_id,
905
- activeSessionId: activeSession?.session_id ?? null,
906
- reason: args.reason || 'force ownership override',
907
- worktreePath: derivedWorktree,
908
- });
909
- console.log(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Claim-session ownership overridden with --force; audit checkpoint recorded.`);
910
- }
911
- const ownershipCheck = await checkOwnership(id, docForValidation, derivedWorktree, args.overrideOwner, args.reason);
912
- if (!ownershipCheck.valid) {
913
- die(ownershipCheck.error ?? 'Ownership check failed');
914
- }
915
- // If override was used, log to audit trail and add to WU notes
916
- if (ownershipCheck.auditEntry) {
917
- auditOwnershipOverride(ownershipCheck.auditEntry);
918
- // Add override reason to WU notes (schema requires string, not array)
919
- const overrideNote = `Ownership override: Completed by ${ownershipCheck.auditEntry.completed_by} (assigned to ${ownershipCheck.auditEntry.assigned_to}). Reason: ${args.reason}`;
920
- appendNote(docForValidation, overrideNote);
921
- // Write updated WU YAML back to worktree
922
- if (derivedWorktree) {
923
- const wtWUPath = path.join(derivedWorktree, WU_PATHS.WU(id));
924
- if (existsSync(wtWUPath)) {
925
- writeWU(wtWUPath, docForValidation);
926
- }
862
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Claim-session ownership overridden with --force; audit checkpoint recorded.`);
863
+ }
864
+ const ownershipCheck = await checkOwnership(id, docForValidation, derivedWorktree, args.overrideOwner, args.reason);
865
+ if (!ownershipCheck.valid) {
866
+ die(ownershipCheck.error ?? 'Ownership check failed');
867
+ }
868
+ if (ownershipCheck.auditEntry) {
869
+ auditOwnershipOverride(ownershipCheck.auditEntry);
870
+ const overrideNote = `Ownership override: Completed by ${ownershipCheck.auditEntry.completed_by} (assigned to ${ownershipCheck.auditEntry.assigned_to}). Reason: ${args.reason}`;
871
+ appendNote(docForValidation, overrideNote);
872
+ if (derivedWorktree) {
873
+ const wtWUPath = path.join(derivedWorktree, WU_PATHS.WU(id));
874
+ if (existsSync(wtWUPath)) {
875
+ writeWU(wtWUPath, docForValidation);
927
876
  }
928
877
  }
929
878
  }
930
- // WU-1280: Early spec completeness validation (before gates)
931
- // Catches missing tests.manual, empty code_paths, etc. BEFORE 2min gate run
879
+ }
880
+ /** WU-1280: Early spec completeness validation (before gates). */
881
+ function preflightValidateSpecCompleteness(id, docForValidation) {
932
882
  console.log(`\n${LOG_PREFIX.DONE} Validating spec completeness for ${id}...`);
933
883
  const specResult = validateSpecCompleteness(docForValidation, id);
934
884
  if (!specResult.valid) {
935
885
  console.error(`\n❌ Spec completeness validation failed for ${id}:\n`);
936
886
  specResult.errors.forEach((err) => console.error(` - ${err}`));
937
- // WU-1311: Use config-based path in error message
938
887
  const specConfig = getConfig();
939
888
  console.error(`\nFix these issues before running wu:done:\n` +
940
889
  ` 1. Update ${specConfig.directories.wuDir}/${id}.yaml\n` +
@@ -947,33 +896,18 @@ async function executePreFlightChecks({ id, args, isBranchOnly, isDocsOnly, docM
947
896
  die(`Cannot mark ${id} as done - spec incomplete`);
948
897
  }
949
898
  console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Spec completeness check passed`);
950
- // WU-1351: Validate code_paths files exist (prevents false completions)
951
- // In worktree mode: validate files exist in worktree (will be merged)
952
- // In branch-only mode: validate files exist on current branch
899
+ // WU-1351: Validate code_paths files exist
953
900
  console.log(`\n${LOG_PREFIX.DONE} Validating code_paths existence for ${id}...`);
954
- const codePathsResult = await validateCodePathsExist(docForValidation, id, {
955
- worktreePath: derivedWorktree,
956
- targetBranch: isBranchOnly ? 'HEAD' : BRANCHES.MAIN,
957
- });
958
- if ('valid' in codePathsResult && !codePathsResult.valid) {
959
- console.error(`\n❌ code_paths validation failed for ${id}:\n`);
960
- if ('errors' in codePathsResult) {
961
- codePathsResult.errors.forEach((err) => console.error(err));
962
- }
963
- die(`Cannot mark ${id} as done - code_paths missing from target branch`);
964
- }
965
- // WU-1324 + WU-1542: Check mandatory agent compliance
966
- // WU-1542: --require-agents makes this a BLOCKING check
967
- const codePaths = docForValidation.code_paths || [];
901
+ }
902
+ /** WU-1324 + WU-1542: Check mandatory agent compliance. */
903
+ function preflightCheckMandatoryAgents(id, args, codePaths) {
968
904
  const compliance = checkMandatoryAgentsComplianceBlocking(codePaths, id, {
969
905
  blocking: Boolean(args.requireAgents),
970
906
  });
971
907
  if (compliance.blocking && compliance.errorMessage) {
972
- // WU-1542: Blocking mode - fail wu:done with detailed error
973
908
  die(compliance.errorMessage);
974
909
  }
975
910
  else if (!compliance.compliant) {
976
- // Non-blocking mode - show warning (original WU-1324 behavior)
977
911
  console.warn(`\n${LOG_PREFIX.DONE} ${EMOJI.WARNING} MANDATORY AGENT WARNING`);
978
912
  console.warn(`The following mandatory agents were not confirmed as invoked:`);
979
913
  for (const agent of compliance.missing) {
@@ -982,6 +916,72 @@ async function executePreFlightChecks({ id, args, isBranchOnly, isDocsOnly, docM
982
916
  console.warn(`\nThis is a NON-BLOCKING warning.`);
983
917
  console.warn(`Use --require-agents to make this a blocking error.\n`);
984
918
  }
919
+ }
920
+ /** Validate --skip-gates requirements: --reason and --fix-wu are mandatory. */
921
+ function preflightValidateSkipGatesRequirements(args) {
922
+ if (!args.skipGates)
923
+ return;
924
+ if (!args.reason) {
925
+ die('--skip-gates requires --reason "<explanation of why gates are being skipped>"');
926
+ }
927
+ if (!args.fixWu) {
928
+ die('--skip-gates requires --fix-wu WU-{id} (the WU that will fix the failing tests)');
929
+ }
930
+ if (!PATTERNS.WU_ID.test(args.fixWu.toUpperCase())) {
931
+ die(`Invalid --fix-wu value '${args.fixWu}'. Expected format: WU-123`);
932
+ }
933
+ }
934
+ // ── End of extracted validators ──
935
+ async function executePreFlightChecks({ id, args, isBranchOnly, isDocsOnly, docMain, docForValidation, derivedWorktree, }) {
936
+ // WU-2400: Delegates to named validator functions to reduce cognitive complexity.
937
+ const schemaResult = preflightValidateYamlSchema(docForValidation);
938
+ // schemaResult.data is guaranteed defined: preflightValidateYamlSchema die()s on failure
939
+ preflightValidateApprovalGates(id, schemaResult.data);
940
+ await preflightValidateCodePathsConsistency(id, docForValidation, derivedWorktree);
941
+ // Tripwire: Scan commands log for violations
942
+ runTripwireCheck();
943
+ await preflightValidateBacklogAndStateConsistency(id);
944
+ await preflightValidateWorktreeState({
945
+ id,
946
+ args,
947
+ isBranchOnly,
948
+ docMain,
949
+ docForValidation,
950
+ derivedWorktree,
951
+ });
952
+ // Use worktree title for commit message (not stale main title)
953
+ const title = docForValidation.title || docMain.title || '';
954
+ if (isDocsOnly) {
955
+ console.log('\n📝 Docs-only WU detected');
956
+ console.log(' - Gates will skip lint/typecheck/tests');
957
+ console.log(' - Only docs/markdown paths allowed\n');
958
+ }
959
+ if (isBranchOnly) {
960
+ console.log('\n🌿 Branch-Only mode detected');
961
+ console.log(' - Gates run in main checkout on lane branch');
962
+ console.log(' - No worktree to remove\n');
963
+ }
964
+ await preflightValidateOwnership({
965
+ id,
966
+ args,
967
+ isBranchOnly,
968
+ docForValidation,
969
+ derivedWorktree,
970
+ });
971
+ preflightValidateSpecCompleteness(id, docForValidation);
972
+ // code_paths existence check (continuation of spec completeness)
973
+ const codePathsResult = await validateCodePathsExist(docForValidation, id, {
974
+ worktreePath: derivedWorktree,
975
+ targetBranch: isBranchOnly ? 'HEAD' : BRANCHES.MAIN,
976
+ });
977
+ if ('valid' in codePathsResult && !codePathsResult.valid) {
978
+ console.error(`\n❌ code_paths validation failed for ${id}:\n`);
979
+ if ('errors' in codePathsResult) {
980
+ codePathsResult.errors.forEach((err) => console.error(err));
981
+ }
982
+ die(`Cannot mark ${id} as done - code_paths missing from target branch`);
983
+ }
984
+ preflightCheckMandatoryAgents(id, args, docForValidation.code_paths || []);
985
985
  // WU-1012: Validate --docs-only flag usage (BLOCKING)
986
986
  const docsOnlyValidation = validateDocsOnlyFlag(docForValidation, { docsOnly: args.docsOnly });
987
987
  if (!docsOnlyValidation.valid) {
@@ -995,18 +995,7 @@ async function executePreFlightChecks({ id, args, isBranchOnly, isDocsOnly, docM
995
995
  });
996
996
  // Run WU validator
997
997
  runWUValidator(docForValidation, id, args.allowTodo, derivedWorktree);
998
- // Validate skip-gates requirements
999
- if (args.skipGates) {
1000
- if (!args.reason) {
1001
- die('--skip-gates requires --reason "<explanation of why gates are being skipped>"');
1002
- }
1003
- if (!args.fixWu) {
1004
- die('--skip-gates requires --fix-wu WU-{id} (the WU that will fix the failing tests)');
1005
- }
1006
- if (!PATTERNS.WU_ID.test(args.fixWu.toUpperCase())) {
1007
- die(`Invalid --fix-wu value '${args.fixWu}'. Expected format: WU-123`);
1008
- }
1009
- }
998
+ preflightValidateSkipGatesRequirements(args);
1010
999
  return { title, docForValidation };
1011
1000
  }
1012
1001
  /**
@@ -1041,101 +1030,73 @@ function printStateHUD({ id, docMain, isBranchOnly, isDocsOnly, derivedWorktree,
1041
1030
  const worktreeDisplay = isBranchOnly ? 'none' : derivedWorktree || 'none';
1042
1031
  console.log(`\n${LOG_PREFIX.DONE} HUD: WU=${id} status=${yamlStatus} stamp=${stampExists} locked=${yamlLocked} mode=${mode} branch=${branch} worktree=${worktreeDisplay}`);
1043
1032
  }
1044
- // eslint-disable-next-line sonarjs/cognitive-complexity -- Pre-existing complexity, refactor tracked separately
1045
- export async function main() {
1046
- // Allow pre-push hook to recognize wu:done automation (WU-1030)
1047
- process.env[ENV_VARS.WU_TOOL] = 'wu-done';
1048
- // Validate CLI arguments and WU ID format (extracted to wu-done-validators.ts)
1049
- const { args, id } = validateInputs(process.argv);
1050
- // WU-1223: Check if running from worktree - wu:done now requires main checkout
1051
- // Agents should use wu:prep from worktree, then wu:done from main
1052
- const { LOCATION_TYPES } = CONTEXT_VALIDATION;
1053
- const currentLocation = await resolveLocation();
1054
- if (currentLocation.type === LOCATION_TYPES.WORKTREE) {
1055
- die(`${EMOJI.FAILURE} wu:done must be run from main checkout, not from a worktree.\n\n` +
1056
- `Current location: ${currentLocation.cwd}\n\n` +
1057
- `WU-1223 NEW WORKFLOW:\n` +
1058
- ` 1. From worktree, run: pnpm wu:prep --id ${id}\n` +
1059
- ` (This runs gates and prepares for completion)\n\n` +
1060
- ` 2. From main, run: cd ${currentLocation.mainCheckout} && pnpm wu:done --id ${id}\n` +
1061
- ` (This does merge + cleanup only)\n\n` +
1062
- `Use wu:prep to run gates in the worktree, then wu:done from main for merge/cleanup.`);
1033
+ // ── WU-2400: Extracted path-specific handlers from main() ──
1034
+ /**
1035
+ * WU-2211: Handle --already-merged early exit path.
1036
+ * Skips merge phase, gates, worktree detection. Only writes metadata.
1037
+ */
1038
+ async function executeAlreadyMergedFinalizePath(id, docMain) {
1039
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.INFO} WU-2211: --already-merged mode activated`);
1040
+ // Safety check: verify code_paths exist on HEAD of main
1041
+ const codePaths = docMain.code_paths || [];
1042
+ const verification = await verifyCodePathsOnMainHead(codePaths);
1043
+ if (!verification.valid) {
1044
+ die(`${EMOJI.FAILURE} --already-merged safety check failed\n\n` +
1045
+ `${verification.error}\n\n` +
1046
+ `Cannot finalize ${id}: code_paths must exist on HEAD before using --already-merged.`);
1047
+ }
1048
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Safety check passed: all ${codePaths.length} code_paths verified on HEAD`);
1049
+ // Execute finalize-only path
1050
+ const title = String(docMain.title || id);
1051
+ const lane = String(docMain.lane || '');
1052
+ const finalizeResult = await executeAlreadyMergedFinalizeFromModule({
1053
+ id,
1054
+ title,
1055
+ lane,
1056
+ doc: docMain,
1057
+ });
1058
+ if (!finalizeResult.success) {
1059
+ die(`${EMOJI.FAILURE} --already-merged finalization failed\n\n` +
1060
+ `Errors:\n${finalizeResult.errors.map((e) => ` - ${e}`).join('\n')}\n\n` +
1061
+ `Partial state may remain. Rerun: pnpm wu:done --id ${id} --already-merged`);
1063
1062
  }
1064
- // Detect workspace mode and calculate paths (WU-1215: extracted to validators module)
1065
- const pathInfo = await detectModeAndPaths(id, args);
1066
- const { WU_PATH, STATUS_PATH, BACKLOG_PATH, STAMPS_DIR, docMain: docMainRaw, isBranchOnly,
1067
- // WU-1492: Detect branch-pr mode for separate completion path
1068
- isBranchPR, derivedWorktree, docForValidation: initialDocForValidationRaw, isDocsOnly, } = pathInfo;
1069
- const docMain = normalizeWUDocLike(docMainRaw);
1070
- const initialDocForValidation = normalizeWUDocLike(initialDocForValidationRaw);
1071
- // Capture main checkout path once. process.cwd() may drift later during recovery flows.
1072
- const mainCheckoutPath = process.cwd();
1073
- // ──────────────────────────────────────────────
1074
- // WU-2211: --already-merged early exit path
1075
- // Skips merge phase, gates, worktree detection. Only writes metadata.
1076
- // ──────────────────────────────────────────────
1077
- if (args.alreadyMerged) {
1078
- console.log(`${LOG_PREFIX.DONE} ${EMOJI.INFO} WU-2211: --already-merged mode activated`);
1079
- // Safety check: verify code_paths exist on HEAD of main
1080
- const codePaths = docMain.code_paths || [];
1081
- const verification = await verifyCodePathsOnMainHead(codePaths);
1082
- if (!verification.valid) {
1083
- die(`${EMOJI.FAILURE} --already-merged safety check failed\n\n` +
1084
- `${verification.error}\n\n` +
1085
- `Cannot finalize ${id}: code_paths must exist on HEAD before using --already-merged.`);
1086
- }
1087
- console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Safety check passed: all ${codePaths.length} code_paths verified on HEAD`);
1088
- // Execute finalize-only path
1089
- const title = String(docMain.title || id);
1090
- const lane = String(docMain.lane || '');
1091
- const finalizeResult = await executeAlreadyMergedFinalizeFromModule({
1092
- id,
1093
- title,
1094
- lane,
1095
- doc: docMain,
1096
- });
1097
- if (!finalizeResult.success) {
1098
- die(`${EMOJI.FAILURE} --already-merged finalization failed\n\n` +
1099
- `Errors:\n${finalizeResult.errors.map((e) => ` - ${e}`).join('\n')}\n\n` +
1100
- `Partial state may remain. Rerun: pnpm wu:done --id ${id} --already-merged`);
1101
- }
1102
- // Release lane lock (non-blocking, same as normal wu:done)
1103
- try {
1104
- const lane = docMain.lane;
1105
- if (lane) {
1106
- const releaseResult = releaseLaneLock(lane, { wuId: id });
1107
- if (releaseResult.released && !releaseResult.notFound) {
1108
- console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Lane lock released for "${lane}"`);
1109
- }
1063
+ // Release lane lock (non-blocking)
1064
+ try {
1065
+ const lane = docMain.lane;
1066
+ if (lane) {
1067
+ const releaseResult = releaseLaneLock(lane, { wuId: id });
1068
+ if (releaseResult.released && !releaseResult.notFound) {
1069
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Lane lock released for "${lane}"`);
1110
1070
  }
1111
1071
  }
1112
- catch (err) {
1113
- console.warn(`${LOG_PREFIX.DONE} Warning: Could not release lane lock: ${getErrorMessage(err)}`);
1114
- }
1115
- // End agent session (non-blocking)
1116
- try {
1117
- endSessionForWU();
1118
- }
1119
- catch {
1120
- // Non-blocking
1121
- }
1122
- // Broadcast completion signal (non-blocking)
1123
- await broadcastCompletionSignal(id, title);
1124
- console.log(`\n${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} ${id} finalized via --already-merged`);
1125
- console.log(`- WU: ${id} -- ${title}`);
1126
- clearConfigCache();
1127
- process.exit(EXIT_CODES.SUCCESS);
1128
1072
  }
1073
+ catch (err) {
1074
+ console.warn(`${LOG_PREFIX.DONE} Warning: Could not release lane lock: ${getErrorMessage(err)}`);
1075
+ }
1076
+ // End agent session (non-blocking)
1077
+ try {
1078
+ endSessionForWU();
1079
+ }
1080
+ catch {
1081
+ // Non-blocking
1082
+ }
1083
+ // Broadcast completion signal (non-blocking)
1084
+ await broadcastCompletionSignal(id, title);
1085
+ console.log(`\n${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} ${id} finalized via --already-merged`);
1086
+ console.log(`- WU: ${id} -- ${title}`);
1087
+ clearConfigCache();
1088
+ process.exit(EXIT_CODES.SUCCESS);
1089
+ }
1090
+ /**
1091
+ * WU-2400: The normal wu:done pipeline (validation, gates, completion, cleanup).
1092
+ * Extracted from main() to reduce cognitive complexity.
1093
+ */
1094
+ async function executeNormalWuDonePath(params) {
1095
+ const { id, args, docMain, initialDocForValidation, isBranchOnly, isBranchPR, derivedWorktree, isDocsOnly, mainCheckoutPath, WU_PATH, STATUS_PATH, BACKLOG_PATH, STAMPS_DIR, } = params;
1129
1096
  // WU-1663: Determine prepPassed early for pipeline actor input.
1130
- // canSkipGates checks if wu:prep already ran gates successfully via checkpoint.
1131
- // This drives the isPrepPassed guard on the GATES_SKIPPED transition.
1132
- // WU-2102: Look for checkpoint in worktree (where wu:prep writes it), not main
1133
1097
  const earlySkipResult = await resolveCheckpointSkipResult(id, derivedWorktree || null);
1134
1098
  const prepPassed = earlySkipResult.canSkip;
1135
1099
  // WU-1663: Create XState pipeline actor for state-driven orchestration.
1136
- // The actor tracks which pipeline stage we're in (validating, gating, committing, etc.)
1137
- // and provides explicit state/transition contracts. Existing procedural logic continues
1138
- // to do the real work; the actor provides structured state tracking alongside it.
1139
1100
  const pipelineActor = createActor(wuDoneMachine, {
1140
1101
  input: {
1141
1102
  wuId: id,
@@ -1144,7 +1105,6 @@ export async function main() {
1144
1105
  },
1145
1106
  });
1146
1107
  pipelineActor.start();
1147
- // WU-1663: Send START event to transition from idle -> validating
1148
1108
  pipelineActor.send({
1149
1109
  type: WU_DONE_EVENTS.START,
1150
1110
  wuId: id,
@@ -1159,7 +1119,6 @@ export async function main() {
1159
1119
  : null;
1160
1120
  const worktreeExists = resolvedWorktreePath ? existsSync(resolvedWorktreePath) : false;
1161
1121
  const { allowFallback: allowBranchOnlyFallback, effectiveBranchOnly } = computeBranchOnlyFallback({
1162
- // WU-1590: Treat branch-pr like branch-only for fallback computation
1163
1122
  isBranchOnly: isNoWorktreeMode,
1164
1123
  branchOnlyRequested: args.branchOnly,
1165
1124
  worktreeExists,
@@ -1181,18 +1140,15 @@ export async function main() {
1181
1140
  die(mainMutationGuard.message ?? 'wu:done blocked by dirty-main guard.');
1182
1141
  }
1183
1142
  // WU-2327: Verify current-session wu:brief evidence before pre-flight restores
1184
- // the tracked worktree wu-events log used to keep auto-rebase safe.
1185
1143
  await enforceWuBriefEvidenceForDone(id, docMain, {
1186
1144
  baseDir: effectiveWorktreePath || mainCheckoutPath,
1187
1145
  force: Boolean(args.force),
1188
1146
  });
1189
1147
  // WU-1169: Ensure worktree is clean before proceeding
1190
- // This prevents WU-1943 rollback loops if rebase fails due to dirty state
1191
1148
  if (effectiveWorktreePath && existsSync(effectiveWorktreePath)) {
1192
1149
  await ensureCleanWorktree(effectiveWorktreePath);
1193
1150
  }
1194
- // Pre-flight checks (WU-1215: extracted to executePreFlightChecks function)
1195
- // WU-1663: Wrap in try/catch to send pipeline failure event before die() propagates
1151
+ // Pre-flight checks
1196
1152
  let preFlightResult;
1197
1153
  try {
1198
1154
  preFlightResult = await executePreFlightChecks({
@@ -1214,18 +1170,15 @@ export async function main() {
1214
1170
  throw preFlightErr;
1215
1171
  }
1216
1172
  const title = preFlightResult.title;
1217
- // Note: docForValidation is returned but not used after pre-flight checks
1218
- // The metadata transaction uses docForUpdate instead
1219
- // WU-1663: Pre-flight checks passed - transition to preparing state
1220
1173
  pipelineActor.send({ type: WU_DONE_EVENTS.VALIDATION_PASSED });
1221
1174
  // WU-1599: Enforce auditable spawn provenance for initiative-governed WUs.
1222
1175
  await enforceSpawnProvenanceForDone(id, docMain, {
1223
1176
  baseDir: mainCheckoutPath,
1224
1177
  force: Boolean(args.force),
1225
1178
  });
1226
- // Step 0: Run gates (WU-1215: extracted to executeGates function)
1179
+ // Step 0: Run gates
1227
1180
  const worktreePath = effectiveWorktreePath;
1228
- // WU-1471 AC3 + WU-1998: Config-driven checkpoint gate with accurate warn-mode messaging.
1181
+ // WU-1471 AC3 + WU-1998: Config-driven checkpoint gate
1229
1182
  const checkpointGateConfig = getConfig();
1230
1183
  const requireCheckpoint = resolveCheckpointGateMode(checkpointGateConfig.memory?.enforcement?.require_checkpoint_for_done);
1231
1184
  await enforceCheckpointGateForDone({
@@ -1233,13 +1186,11 @@ export async function main() {
1233
1186
  workspacePath: worktreePath || mainCheckoutPath,
1234
1187
  mode: requireCheckpoint,
1235
1188
  });
1236
- // WU-1663: Preparation complete - transition to gating state
1237
1189
  pipelineActor.send({ type: WU_DONE_EVENTS.PREPARATION_COMPLETE });
1238
1190
  // WU-2102: Resolve scoped test paths from WU spec tests.unit for gate fallback
1239
1191
  const scopedTestPathsForDone = resolveScopedUnitTestsForPrep({
1240
1192
  tests: docMain.tests,
1241
1193
  });
1242
- // WU-1663: Wrap gates in try/catch to send pipeline failure event
1243
1194
  let gateExecutionResult;
1244
1195
  try {
1245
1196
  gateExecutionResult = await executeGates({
@@ -1264,15 +1215,12 @@ export async function main() {
1264
1215
  pipelineActor.stop();
1265
1216
  throw gateErr;
1266
1217
  }
1267
- // WU-1663: Gates passed - transition from gating state.
1268
- // Use GATES_SKIPPED if checkpoint dedup allowed skip, GATES_PASSED otherwise.
1269
1218
  if (gateExecutionResult.skippedByCheckpoint) {
1270
1219
  pipelineActor.send({ type: WU_DONE_EVENTS.GATES_SKIPPED });
1271
1220
  }
1272
1221
  else {
1273
1222
  pipelineActor.send({ type: WU_DONE_EVENTS.GATES_PASSED });
1274
1223
  }
1275
- // Print State HUD for visibility (WU-1215: extracted to printStateHUD function)
1276
1224
  printStateHUD({
1277
1225
  id,
1278
1226
  docMain,
@@ -1294,7 +1242,6 @@ export async function main() {
1294
1242
  runGatesFn: ({ cwd }) => runGates({ cwd, docsOnly: false }),
1295
1243
  });
1296
1244
  // Step 1: Execute mode-specific completion workflow (WU-2167)
1297
- // Main remains orchestration-only; execution details live in wu-done-mode-execution.ts.
1298
1245
  let completionResult = { cleanupSafe: true };
1299
1246
  if (!args.noAuto) {
1300
1247
  completionResult = await executeModeSpecificCompletion({
@@ -1324,9 +1271,7 @@ export async function main() {
1324
1271
  await ensureNoAutoStagedOrNoop([WU_PATH, STATUS_PATH, BACKLOG_PATH, STAMPS_DIR]);
1325
1272
  }
1326
1273
  // WU-2262: Do not run repository-wide worktree_path sanitation from local main during wu:done.
1327
- // The prior flow could leave staged residue on main when direct-commit guards blocked commit hooks.
1328
- // Step 6 & 7: Cleanup (remove worktree, delete branch) - WU-1215
1329
- // WU-1811: Only run cleanup if all completion steps succeeded
1274
+ // Step 6 & 7: Cleanup (remove worktree, delete branch)
1330
1275
  if (completionResult.cleanupSafe !== false) {
1331
1276
  await runCleanup(docMain, args);
1332
1277
  }
@@ -1334,7 +1279,6 @@ export async function main() {
1334
1279
  console.log(`\n${LOG_PREFIX.DONE} ${EMOJI.WARNING} WU-1811: Skipping worktree cleanup - metadata/push incomplete`);
1335
1280
  }
1336
1281
  // WU-1603: Release lane lock after successful completion
1337
- // This allows the lane to be claimed by another WU
1338
1282
  try {
1339
1283
  const lane = docMain.lane;
1340
1284
  if (lane) {
@@ -1342,11 +1286,9 @@ export async function main() {
1342
1286
  if (releaseResult.released && !releaseResult.notFound) {
1343
1287
  console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Lane lock released for "${lane}"`);
1344
1288
  }
1345
- // Silent if notFound - lock may not exist (older WUs, manual cleanup)
1346
1289
  }
1347
1290
  }
1348
1291
  catch (err) {
1349
- // Non-blocking: lock release failure should not block completion
1350
1292
  console.warn(`${LOG_PREFIX.DONE} Warning: Could not release lane lock: ${getErrorMessage(err)}`);
1351
1293
  }
1352
1294
  // WU-1438: Auto-end agent session
@@ -1355,28 +1297,21 @@ export async function main() {
1355
1297
  if (sessionResult.ended) {
1356
1298
  const sessionId = sessionResult.summary?.session_id;
1357
1299
  if (sessionId) {
1358
- // Emergency fix Session 2: Use SESSION.ID_DISPLAY_LENGTH constant
1359
1300
  console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Agent session ended (${sessionId.slice(0, SESSION.ID_DISPLAY_LENGTH)}...)`);
1360
1301
  }
1361
1302
  }
1362
- // No warning if no active session - silent no-op is expected
1363
1303
  }
1364
1304
  catch (err) {
1365
- // Non-blocking: session end failure should not block completion
1366
1305
  console.warn(`${LOG_PREFIX.DONE} Warning: Could not end agent session: ${getErrorMessage(err)}`);
1367
1306
  }
1368
1307
  // WU-1588: Broadcast completion signal after session end
1369
- // Non-blocking: failures handled internally by broadcastCompletionSignal
1370
1308
  await broadcastCompletionSignal(id, title);
1371
- // WU-1473: Mark completed-WU signals as read using receipt-aware behavior
1372
- // Non-blocking: markCompletedWUSignalsAsRead is fail-open (AC4)
1309
+ // WU-1473: Mark completed-WU signals as read
1373
1310
  const markResult = await markCompletedWUSignalsAsRead(mainCheckoutPath, id);
1374
1311
  if (markResult.markedCount > 0) {
1375
1312
  console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Marked ${markResult.markedCount} signal(s) as read for ${id}`);
1376
1313
  }
1377
1314
  // WU-1946: Update spawn registry to mark WU as completed
1378
- // Non-blocking: failures handled internally by updateSpawnRegistryOnCompletion
1379
- // Works in both worktree and branch-only modes (called after completionResult)
1380
1315
  await updateSpawnRegistryOnCompletion(id, mainCheckoutPath);
1381
1316
  await flushWuLifecycleSync({
1382
1317
  command: WU_LIFECYCLE_COMMANDS.DONE,
@@ -1388,13 +1323,10 @@ export async function main() {
1388
1323
  },
1389
1324
  });
1390
1325
  // WU-1747: Clear checkpoint on successful completion
1391
- // Checkpoint is no longer needed once WU is fully complete
1392
1326
  clearCheckpoint(id, { baseDir: worktreePath || undefined });
1393
1327
  // WU-1471 AC4: Remove per-WU hook counter file on completion
1394
- // Fail-safe: cleanupHookCounters never throws
1395
1328
  cleanupHookCounters(mainCheckoutPath, id);
1396
1329
  // WU-1474: Invoke decay archival when memory.decay policy is configured
1397
- // Non-blocking: errors are captured but never block wu:done completion (fail-open)
1398
1330
  try {
1399
1331
  const decayConfig = getConfig().memory?.decay;
1400
1332
  const decayResult = await runDecayOnDone(mainCheckoutPath, decayConfig);
@@ -1406,27 +1338,20 @@ export async function main() {
1406
1338
  }
1407
1339
  }
1408
1340
  catch (err) {
1409
- // Double fail-open: even if runDecayOnDone itself throws unexpectedly, never block wu:done
1410
1341
  console.warn(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Decay archival error (fail-open): ${getErrorMessage(err)}`);
1411
1342
  }
1412
1343
  // WU-1663: Cleanup complete - transition to final done state
1413
1344
  pipelineActor.send({ type: WU_DONE_EVENTS.CLEANUP_COMPLETE });
1414
- // WU-1663: Log final pipeline state for diagnostics
1415
1345
  const finalSnapshot = pipelineActor.getSnapshot();
1416
1346
  console.log(`${LOG_PREFIX.DONE} Pipeline state: ${finalSnapshot.value} (WU-1663)`);
1417
1347
  pipelineActor.stop();
1418
1348
  console.log(`\n${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Transaction COMMIT - all steps succeeded (WU-755)`);
1419
1349
  console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Marked done, pushed, and cleaned up.`);
1420
1350
  console.log(`- WU: ${id} — ${title}`);
1421
- // WU-2126: Invalidate config cache so subsequent commands in the same process
1422
- // read fresh values from disk (wu:done may have mutated workspace.yaml/state).
1423
1351
  clearConfigCache();
1424
1352
  // WU-1763: Print lifecycle nudges (conditional, non-blocking)
1425
- // Discovery summary nudge - only if discoveries exist
1426
1353
  const discoveries = await loadDiscoveriesForWU(mainCheckoutPath, id);
1427
1354
  printDiscoveryNudge(id, discoveries.count, discoveries.ids);
1428
- // Documentation validation nudge - only if docs changed
1429
- // Use worktreePath if available, otherwise skip (branch-only mode has no worktree)
1430
1355
  if (worktreePath) {
1431
1356
  const changedDocs = await detectChangedDocPaths(worktreePath, BRANCHES.MAIN);
1432
1357
  printDocValidationNudge(id, changedDocs);
@@ -1436,17 +1361,58 @@ export async function main() {
1436
1361
  currentBranch !== BRANCHES.MAIN &&
1437
1362
  currentBranch !== BRANCHES.MASTER;
1438
1363
  if (shouldRunCleanupMutations) {
1439
- // WU-1366: Auto state cleanup after successful completion
1440
- // Non-fatal: errors are logged but do not block completion
1441
1364
  await runAutoCleanupAfterDone(mainCheckoutPath);
1442
- // WU-1533: Auto-commit dirty state files left by cleanup.
1443
- // Branch-aware: in branch-pr mode this stays on the lane branch.
1444
1365
  await commitCleanupChanges({ targetBranch: currentBranch });
1445
1366
  }
1446
1367
  else {
1447
1368
  console.log(`${LOG_PREFIX.DONE} ${EMOJI.INFO} WU-1611: Skipping auto-cleanup mutations on protected branch ${currentBranch}`);
1448
1369
  }
1449
1370
  }
1371
+ // ── End of extracted path-specific handlers ──
1372
+ export async function main() {
1373
+ // Allow pre-push hook to recognize wu:done automation (WU-1030)
1374
+ process.env[ENV_VARS.WU_TOOL] = 'wu-done';
1375
+ // Validate CLI arguments and WU ID format
1376
+ const { args, id } = validateInputs(process.argv);
1377
+ // WU-1223: Check if running from worktree - wu:done requires main checkout
1378
+ const { LOCATION_TYPES } = CONTEXT_VALIDATION;
1379
+ const currentLocation = await resolveLocation();
1380
+ if (currentLocation.type === LOCATION_TYPES.WORKTREE) {
1381
+ die(`${EMOJI.FAILURE} wu:done must be run from main checkout, not from a worktree.\n\n` +
1382
+ `Current location: ${currentLocation.cwd}\n\n` +
1383
+ `WU-1223 NEW WORKFLOW:\n` +
1384
+ ` 1. From worktree, run: pnpm wu:prep --id ${id}\n` +
1385
+ ` (This runs gates and prepares for completion)\n\n` +
1386
+ ` 2. From main, run: cd ${currentLocation.mainCheckout} && pnpm wu:done --id ${id}\n` +
1387
+ ` (This does merge + cleanup only)\n\n` +
1388
+ `Use wu:prep to run gates in the worktree, then wu:done from main for merge/cleanup.`);
1389
+ }
1390
+ // Detect workspace mode and calculate paths
1391
+ const pathInfo = await detectModeAndPaths(id, args);
1392
+ const { WU_PATH, STATUS_PATH, BACKLOG_PATH, STAMPS_DIR, docMain: docMainRaw, isBranchOnly, isBranchPR, derivedWorktree, docForValidation: initialDocForValidationRaw, isDocsOnly, } = pathInfo;
1393
+ const docMain = normalizeWUDocLike(docMainRaw);
1394
+ const initialDocForValidation = normalizeWUDocLike(initialDocForValidationRaw);
1395
+ const mainCheckoutPath = process.cwd();
1396
+ // WU-2400: Dispatch to extracted path-specific handlers.
1397
+ if (args.alreadyMerged) {
1398
+ await executeAlreadyMergedFinalizePath(id, docMain);
1399
+ }
1400
+ await executeNormalWuDonePath({
1401
+ id,
1402
+ args,
1403
+ docMain,
1404
+ initialDocForValidation,
1405
+ isBranchOnly,
1406
+ isBranchPR,
1407
+ derivedWorktree,
1408
+ isDocsOnly,
1409
+ mainCheckoutPath,
1410
+ WU_PATH,
1411
+ STATUS_PATH,
1412
+ BACKLOG_PATH,
1413
+ STAMPS_DIR,
1414
+ });
1415
+ }
1450
1416
  /**
1451
1417
  * WU-1763: Print discovery summary nudge when discoveries exist for this WU.
1452
1418
  * Conditional output - only prints when discoveryCount > 0.