@lumenflow/cli 3.5.0 → 3.6.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.
package/dist/wu-done.js CHANGED
@@ -57,8 +57,6 @@ import { existsSync, readFileSync, mkdirSync, appendFileSync, unlinkSync, statSy
57
57
  import path from 'node:path';
58
58
  // WU-1825: Import from unified code-path-validator (consolidates 3 validators)
59
59
  import { validateWUCodePaths } from '@lumenflow/core/code-path-validator';
60
- import { validateDocsOnly, getAllowedPathsDescription } from '@lumenflow/core/docs-path-validator';
61
- import { scanLogForViolations, rotateLog } from '@lumenflow/core/commands-logger';
62
60
  import { rollbackFiles } from '@lumenflow/core/rollback-utils';
63
61
  import { validateInputs, detectModeAndPaths, defaultBranchFrom, runCleanup, validateSpecCompleteness, runPreflightTasksValidation, buildPreflightErrorMessage,
64
62
  // WU-1805: Preflight code_paths validation before gates
@@ -70,10 +68,9 @@ validateTypeVsCodePathsPreflight, buildTypeVsCodePathsErrorMessage, } from '@lum
70
68
  import { formatPreflightWarnings } from '@lumenflow/core/wu-preflight-validators';
71
69
  // WU-1825: validateCodePathsExist moved to unified code-path-validator
72
70
  import { validateCodePathsExist } from '@lumenflow/core/code-path-validator';
73
- import { BRANCHES, REMOTES, PATTERNS, DEFAULTS, LOG_PREFIX, EMOJI, GIT, SESSION, WU_STATUS, WU_EXPOSURE, WU_TYPES, PKG_MANAGER, SCRIPTS, CLI_FLAGS, FILE_SYSTEM, EXIT_CODES, STRING_LITERALS, MICRO_WORKTREE_OPERATIONS, TELEMETRY_STEPS, SKIP_GATES_REASONS, CHECKPOINT_MESSAGES, LUMENFLOW_PATHS, ENV_VARS, getWUStatusDisplay,
71
+ import { BRANCHES, PATTERNS, DEFAULTS, LOG_PREFIX, EMOJI, GIT, SESSION, WU_STATUS, PKG_MANAGER, SCRIPTS, CLI_FLAGS, FILE_SYSTEM, EXIT_CODES, STRING_LITERALS, MICRO_WORKTREE_OPERATIONS, TELEMETRY_STEPS, SKIP_GATES_REASONS, CHECKPOINT_MESSAGES, ENV_VARS, getWUStatusDisplay,
74
72
  // WU-1223: Location types for worktree detection
75
73
  CONTEXT_VALIDATION, } from '@lumenflow/core/wu-constants';
76
- import { isDocumentationType } from '@lumenflow/core/wu-type-helpers';
77
74
  import { getDocsOnlyPrefixes, DOCS_ONLY_ROOT_FILES } from '@lumenflow/core';
78
75
  import { printGateFailureBox, printStatusPreview } from '@lumenflow/core/wu-done-ui';
79
76
  import { ensureOnMain } from '@lumenflow/core/wu-helpers';
@@ -88,12 +85,14 @@ executeBranchPRCompletion, } from '@lumenflow/core/wu-done-branch-only';
88
85
  import { executeWorktreeCompletion, autoRebaseBranch } from '@lumenflow/core/wu-done-worktree';
89
86
  // WU-1746: Already-merged worktree resilience
90
87
  import { detectAlreadyMergedNoWorktree, executeAlreadyMergedCompletion, } from '@lumenflow/core/wu-done-merged-worktree';
88
+ // WU-2211: --already-merged finalize-only mode
89
+ import { verifyCodePathsOnMainHead, executeAlreadyMergedFinalize as executeAlreadyMergedFinalizeFromModule, } from './wu-done-already-merged.js';
91
90
  import { checkWUConsistency } from '@lumenflow/core/wu-consistency-checker';
92
91
  // WU-1542: Use blocking mode compliance check (replaces non-blocking checkMandatoryAgentsCompliance)
93
92
  import { checkMandatoryAgentsComplianceBlocking } from '@lumenflow/core/orchestration-rules';
94
93
  import { endSessionForWU } from '@lumenflow/agent/auto-session';
95
94
  import { runBackgroundProcessCheck } from '@lumenflow/core/process-detector';
96
- import { WUStateStore, getLatestWuBriefEvidence } from '@lumenflow/core/wu-state-store';
95
+ import { WUStateStore } from '@lumenflow/core/wu-state-store';
97
96
  // WU-1588: INIT-007 memory layer integration
98
97
  import { createCheckpoint } from '@lumenflow/memory/checkpoint';
99
98
  import { createSignal, loadSignals } from '@lumenflow/memory/signal';
@@ -108,9 +107,6 @@ import { createPreGatesCheckpoint as createWU1747Checkpoint, markGatesPassed, ca
108
107
  // WU-1946: Spawn registry for tracking sub-agent spawns
109
108
  import { DelegationRegistryStore } from '@lumenflow/core/delegation-registry-store';
110
109
  import { DelegationStatus } from '@lumenflow/core/delegation-registry-schema';
111
- // WU-1999: Exposure validation for UI pairing
112
- // WU-2022: Feature accessibility validation (blocking)
113
- import { validateExposure, validateFeatureAccessibility } from '@lumenflow/core/wu-validation';
114
110
  import { ensureCleanWorktree } from './wu-done-check.js';
115
111
  // WU-1366: Auto cleanup after wu:done success
116
112
  // WU-1533: commitCleanupChanges auto-commits dirty state files after cleanup
@@ -122,6 +118,10 @@ import { markCompletedWUSignalsAsRead } from './hooks/enforcement-generator.js';
122
118
  import { evaluateMainDirtyMutationGuard } from './hooks/dirty-guard.js';
123
119
  // WU-1474: Decay policy invocation during completion lifecycle
124
120
  import { runDecayOnDone } from './wu-done-decay.js';
121
+ import { enforceSpawnProvenanceForDone, enforceWuBriefEvidenceForDone, printExposureWarnings, validateAccessibilityOrDie, validateDocsOnlyFlag, } from './wu-done-policies.js';
122
+ import { detectParallelCompletions, ensureNoAutoStagedOrNoop, runTripwireCheck, validateBranchOnlyMode, validateStagedFiles, } from './wu-done-git-ops.js';
123
+ export { buildGatesCommand, buildMissingSpawnPickupEvidenceMessage, buildMissingSpawnProvenanceMessage, buildMissingWuBriefEvidenceMessage, enforceSpawnProvenanceForDone, enforceWuBriefEvidenceForDone, hasSpawnPickupEvidence, printExposureWarnings, shouldEnforceSpawnProvenance, shouldEnforceWuBriefEvidence, validateAccessibilityOrDie, validateDocsOnlyFlag, } from './wu-done-policies.js';
124
+ export { isBranchAlreadyMerged } from './wu-done-git-ops.js';
125
125
  // WU-1588: Memory layer constants
126
126
  const MEMORY_SIGNAL_TYPES = {
127
127
  WU_COMPLETION: 'wu_completion',
@@ -253,101 +253,6 @@ async function validateClaimMetadataBeforeGates(id, worktreePath, yamlStatus) {
253
253
  ` pnpm wu:done --id ${id}\n\n` +
254
254
  `See: https://lumenflow.dev/reference/troubleshooting-wu-done/ for more recovery options.`);
255
255
  }
256
- export function printExposureWarnings(wu, options = {}) {
257
- // Validate exposure
258
- const result = validateExposure(wu, { skipExposureCheck: options.skipExposureCheck });
259
- // Print warnings if present
260
- if (result.warnings.length > 0) {
261
- console.log(`\n${LOG_PREFIX.DONE} ${EMOJI.WARNING} WU-1999: Exposure validation warnings:`);
262
- for (const warning of result.warnings) {
263
- console.log(`${LOG_PREFIX.DONE} ${warning}`);
264
- }
265
- console.log(`${LOG_PREFIX.DONE} These are non-blocking warnings. ` +
266
- `To skip, use --skip-exposure-check flag.\n`);
267
- }
268
- }
269
- export function validateAccessibilityOrDie(wu, options = {}) {
270
- const result = validateFeatureAccessibility(wu, {
271
- skipAccessibilityCheck: options.skipAccessibilityCheck,
272
- });
273
- if (!result.valid) {
274
- console.log(`\n${LOG_PREFIX.DONE} ${EMOJI.FAILURE} WU-2022: Feature accessibility validation failed`);
275
- die(`❌ FEATURE ACCESSIBILITY VALIDATION FAILED (WU-2022)\n\n` +
276
- `Cannot complete wu:done - UI feature accessibility not verified.\n\n` +
277
- `${result.errors.join('\n\n')}\n\n` +
278
- `This gate prevents "orphaned code" - features that exist but users cannot access.`);
279
- }
280
- }
281
- export function validateDocsOnlyFlag(wu, args) {
282
- // If --docs-only flag is not used, no validation needed
283
- if (!args.docsOnly) {
284
- return { valid: true, errors: [] };
285
- }
286
- const wuId = wu.id || 'unknown';
287
- const exposure = wu.exposure;
288
- const type = wu.type;
289
- const codePaths = wu.code_paths;
290
- // Check 1: exposure is 'documentation'
291
- if (exposure === WU_EXPOSURE.DOCUMENTATION) {
292
- return { valid: true, errors: [] };
293
- }
294
- // Check 2: type is 'documentation'
295
- if (isDocumentationType(type)) {
296
- return { valid: true, errors: [] };
297
- }
298
- // Check 3: all code_paths are documentation paths
299
- const docsOnlyPrefixes = getDocsOnlyPrefixes().map((prefix) => prefix.toLowerCase());
300
- const isDocsPath = (p) => {
301
- const path = p.trim().toLowerCase();
302
- // Check docs prefixes
303
- for (const prefix of docsOnlyPrefixes) {
304
- if (path.startsWith(prefix))
305
- return true;
306
- }
307
- // Check markdown files
308
- if (path.endsWith('.md'))
309
- return true;
310
- // Check root file patterns
311
- for (const pattern of DOCS_ONLY_ROOT_FILES) {
312
- if (path.startsWith(pattern))
313
- return true;
314
- }
315
- return false;
316
- };
317
- if (codePaths && Array.isArray(codePaths) && codePaths.length > 0) {
318
- const allDocsOnly = codePaths.every((p) => typeof p === 'string' && isDocsPath(p));
319
- if (allDocsOnly) {
320
- return { valid: true, errors: [] };
321
- }
322
- }
323
- // Validation failed - provide clear error message
324
- const currentExposure = exposure || 'not set';
325
- const currentType = type || 'not set';
326
- return {
327
- valid: false,
328
- errors: [
329
- `--docs-only flag used on ${wuId} but WU is not documentation-focused.\n\n` +
330
- `Current exposure: ${currentExposure}\n` +
331
- `Current type: ${currentType}\n\n` +
332
- `--docs-only requires one of:\n` +
333
- ` 1. exposure: documentation\n` +
334
- ` 2. type: documentation\n` +
335
- ` 3. All code_paths under configured docs prefixes (${docsOnlyPrefixes.join(', ')}), or *.md files\n\n` +
336
- `To fix, either:\n` +
337
- ` - Remove --docs-only flag and run full gates\n` +
338
- ` - Change WU exposure to 'documentation' if this is truly a docs-only change`,
339
- ],
340
- };
341
- }
342
- export function buildGatesCommand(options) {
343
- const { docsOnly = false, isDocsOnly = false } = options;
344
- // Use docs-only gates if either explicit flag or auto-detected
345
- const shouldUseDocsOnly = docsOnly || isDocsOnly;
346
- if (shouldUseDocsOnly) {
347
- return `${PKG_MANAGER} ${SCRIPTS.GATES} -- ${CLI_FLAGS.DOCS_ONLY}`;
348
- }
349
- return `${PKG_MANAGER} ${SCRIPTS.GATES}`;
350
- }
351
256
  async function _assertWorktreeWUInProgressInStateStore(id, worktreePath) {
352
257
  const resolvedWorktreePath = path.resolve(worktreePath);
353
258
  const stateDir = resolveStateDir(resolvedWorktreePath);
@@ -456,159 +361,6 @@ async function checkInboxForRecentSignals(id, baseDir = process.cwd()) {
456
361
  console.warn(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Could not check inbox for signals: ${getErrorMessage(err)}`);
457
362
  }
458
363
  }
459
- /**
460
- * Enforce wu:brief evidence for feature and bug WUs.
461
- */
462
- export function shouldEnforceWuBriefEvidence(doc) {
463
- return doc.type === WU_TYPES.FEATURE || doc.type === WU_TYPES.BUG;
464
- }
465
- /**
466
- * Build remediation guidance when wu:brief evidence is missing.
467
- */
468
- export function buildMissingWuBriefEvidenceMessage(id) {
469
- return (`Missing wu:brief evidence for ${id}.\n\n` +
470
- `Completion policy requires an auditable wu:brief execution record for feature/bug WUs.\n\n` +
471
- `Fix options:\n` +
472
- ` 1. Run wu:brief in the claimed workspace:\n` +
473
- ` pnpm wu:brief --id ${id}\n` +
474
- ` 2. Retry completion:\n` +
475
- ` pnpm wu:done --id ${id}\n` +
476
- ` 3. Legacy/manual override (audited):\n` +
477
- ` pnpm wu:done --id ${id} --force`);
478
- }
479
- function buildWuBriefEvidenceReadFailureMessage(id, stateDir, error) {
480
- return (`Could not verify wu:brief evidence for ${id}.\n\n` +
481
- `State path: ${stateDir}\n` +
482
- `Error: ${getErrorMessage(error)}\n\n` +
483
- `Fix options:\n` +
484
- ` 1. Repair/restore state store, then rerun wu:done\n` +
485
- ` 2. Use --force for audited override when recovery is not possible`);
486
- }
487
- export async function enforceWuBriefEvidenceForDone(id, doc, options = {}) {
488
- if (!shouldEnforceWuBriefEvidence(doc)) {
489
- return;
490
- }
491
- const baseDir = options.baseDir ?? process.cwd();
492
- const force = options.force === true;
493
- const stateDir = resolveStateDir(baseDir);
494
- const getBriefEvidenceFn = options.getBriefEvidenceFn ?? getLatestWuBriefEvidence;
495
- const blocker = options.blocker ?? ((message) => die(message));
496
- const warn = options.warn ?? console.warn;
497
- let evidence;
498
- try {
499
- evidence = await getBriefEvidenceFn(stateDir, id);
500
- }
501
- catch (error) {
502
- if (!force) {
503
- blocker(buildWuBriefEvidenceReadFailureMessage(id, stateDir, error));
504
- return;
505
- }
506
- warn(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} WU-2132: brief evidence verification failed for ${id}, override accepted via --force`);
507
- return;
508
- }
509
- if (evidence) {
510
- return;
511
- }
512
- if (!force) {
513
- blocker(buildMissingWuBriefEvidenceMessage(id));
514
- return;
515
- }
516
- warn(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} WU-2132: brief evidence override accepted for ${id} via --force`);
517
- }
518
- /**
519
- * Returns true when completion should enforce spawn provenance.
520
- * Initiative-linked WUs are expected to carry machine-verifiable spawn lineage.
521
- */
522
- export function shouldEnforceSpawnProvenance(doc) {
523
- return typeof doc?.initiative === 'string' && doc.initiative.trim().length > 0;
524
- }
525
- /**
526
- * Build actionable remediation guidance for missing spawn provenance.
527
- */
528
- export function buildMissingSpawnProvenanceMessage(id, initiativeId) {
529
- return (`Missing spawn provenance for initiative-governed WU ${id} (${initiativeId}).\n\n` +
530
- `This completion path enforces auditable delegation lineage for initiative work.\n\n` +
531
- `Fix options:\n` +
532
- ` 1. Re-run with --force for an audited override (legacy/manual workflow)\n` +
533
- ` 2. Register spawn lineage before completion (preferred):\n` +
534
- ` pnpm wu:delegate --id ${id} --parent-wu WU-XXXX --client codex-cli\n\n` +
535
- `Then retry: pnpm wu:done --id ${id}`);
536
- }
537
- /**
538
- * Build actionable remediation guidance for intent-only spawn provenance
539
- * (delegation intent exists but claim-time pickup evidence is missing).
540
- */
541
- export function buildMissingSpawnPickupEvidenceMessage(id, initiativeId) {
542
- return (`Missing pickup evidence for initiative-governed WU ${id} (${initiativeId}).\n\n` +
543
- `Delegation intent exists, but this WU has no claim-time pickup handshake.\n` +
544
- `Completion policy requires both intent and pickup evidence.\n\n` +
545
- `Fix options:\n` +
546
- ` 1. Re-run with --force for an audited override (legacy/manual claim)\n` +
547
- ` 2. Ensure future delegated work is picked up via wu:claim (records handshake automatically)\n\n` +
548
- `Then retry: pnpm wu:done --id ${id}`);
549
- }
550
- /**
551
- * Returns true when spawn provenance includes claim-time pickup evidence.
552
- */
553
- export function hasSpawnPickupEvidence(spawnEntry) {
554
- const pickedUpAt = typeof spawnEntry?.pickedUpAt === 'string' && spawnEntry.pickedUpAt.trim().length > 0
555
- ? spawnEntry.pickedUpAt
556
- : '';
557
- const pickedUpBy = typeof spawnEntry?.pickedUpBy === 'string' && spawnEntry.pickedUpBy.trim().length > 0
558
- ? spawnEntry.pickedUpBy
559
- : '';
560
- return pickedUpAt.length > 0 && pickedUpBy.length > 0;
561
- }
562
- /**
563
- * Record forced spawn-provenance bypass in memory signals for auditability.
564
- */
565
- async function recordSpawnProvenanceOverride(id, doc, baseDir = process.cwd()) {
566
- try {
567
- const initiativeId = typeof doc?.initiative === 'string' ? doc.initiative.trim() : 'unknown';
568
- const lane = typeof doc?.lane === 'string' ? doc.lane : undefined;
569
- const result = await createSignal(baseDir, {
570
- message: `spawn-provenance override used for ${id} in ${initiativeId} via --force`,
571
- wuId: id,
572
- lane,
573
- });
574
- if (result.success) {
575
- console.log(`${LOG_PREFIX.DONE} ${EMOJI.INFO} Spawn-provenance override recorded (${result.signal.id})`);
576
- }
577
- }
578
- catch (err) {
579
- console.warn(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Could not record spawn-provenance override: ${getErrorMessage(err)}`);
580
- }
581
- }
582
- /**
583
- * Enforce spawn provenance policy for initiative-governed WUs before completion.
584
- */
585
- export async function enforceSpawnProvenanceForDone(id, doc, options = {}) {
586
- if (!shouldEnforceSpawnProvenance(doc)) {
587
- return;
588
- }
589
- const initiativeId = typeof doc.initiative === 'string' && doc.initiative.trim() ? doc.initiative.trim() : 'unknown';
590
- const baseDir = options.baseDir ?? process.cwd();
591
- const force = options.force === true;
592
- const store = new DelegationRegistryStore(resolveStateDir(baseDir));
593
- await store.load();
594
- const spawnEntry = store.getByTarget(id);
595
- if (!spawnEntry) {
596
- if (!force) {
597
- die(buildMissingSpawnProvenanceMessage(id, initiativeId));
598
- }
599
- console.warn(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} WU-1599: spawn provenance override accepted for ${id} (${initiativeId}) via --force`);
600
- await recordSpawnProvenanceOverride(id, doc, baseDir);
601
- return;
602
- }
603
- if (hasSpawnPickupEvidence(spawnEntry)) {
604
- return;
605
- }
606
- if (!force) {
607
- die(buildMissingSpawnPickupEvidenceMessage(id, initiativeId));
608
- }
609
- console.warn(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} WU-1605: pickup evidence override accepted for ${id} (${initiativeId}) via --force`);
610
- await recordSpawnProvenanceOverride(id, doc, baseDir);
611
- }
612
364
  /**
613
365
  * WU-1946: Update spawn registry on WU completion.
614
366
  * Non-blocking wrapper - failures logged as warnings.
@@ -668,38 +420,6 @@ export function normalizeUsername(value) {
668
420
  const username = atIndex > 0 ? str.slice(0, atIndex) : str;
669
421
  return username.toLowerCase();
670
422
  }
671
- /**
672
- * WU-1234: Detect if branch is already merged to main
673
- * Checks if branch tip is an ancestor of main HEAD (i.e., already merged).
674
- * This prevents merge loops when code was merged via emergency fix or manual merge.
675
- *
676
- * @param {string} branch - Lane branch name
677
- * @returns {Promise<boolean>} True if branch is already merged to main
678
- */
679
- export async function isBranchAlreadyMerged(branch) {
680
- try {
681
- const gitAdapter = getGitForCwd();
682
- const branchTip = (await gitAdapter.getCommitHash(branch)).trim();
683
- const mergeBase = (await gitAdapter.mergeBase(BRANCHES.MAIN, branch)).trim();
684
- const mainHead = (await gitAdapter.getCommitHash(BRANCHES.MAIN)).trim();
685
- // Branch is already merged if:
686
- // 1. Branch tip equals merge-base (branch has been rebased/merged onto main)
687
- // 2. Branch tip is an ancestor of main HEAD
688
- if (branchTip === mergeBase) {
689
- // Emergency fix Session 2: Use GIT.SHA_SHORT_LENGTH constant
690
- console.log(`${LOG_PREFIX.DONE} ${EMOJI.INFO} Branch ${branch} is already merged to main\n` +
691
- ` Branch tip: ${branchTip.substring(0, GIT.SHA_SHORT_LENGTH)}\n` +
692
- ` Merge-base: ${mergeBase.substring(0, GIT.SHA_SHORT_LENGTH)}\n` +
693
- ` Main HEAD: ${mainHead.substring(0, GIT.SHA_SHORT_LENGTH)}`);
694
- return true;
695
- }
696
- return false;
697
- }
698
- catch (e) {
699
- console.warn(`${LOG_PREFIX.DONE} Could not check if branch is already merged: ${getErrorMessage(e)}`);
700
- return false;
701
- }
702
- }
703
423
  // WU-1281: isDocsOnlyByPaths removed - use shouldSkipWebTests from path-classifiers.ts
704
424
  // The validators already use shouldSkipWebTests via detectDocsOnlyByPaths wrapper.
705
425
  // Keeping the export for backward compatibility but re-exporting the canonical function.
@@ -757,278 +477,6 @@ function getCommitHeaderLimit() {
757
477
  }
758
478
  }
759
479
  // ensureOnMain() moved to wu-helpers.ts (WU-1256)
760
- /**
761
- * Ensure working tree is clean before wu:done operations.
762
- *
763
- * Prevents multi-agent data loss: If uncommitted files exist in main
764
- * checkout, wu:done operations may fail mid-workflow. Agents then
765
- * automatically "clean up" by running git reset/clean, destroying
766
- * other agents' uncommitted work.
767
- *
768
- * This check HALTS wu:done immediately and guides the agent to verify
769
- * ownership before proceeding.
770
- *
771
- * Context: WU-635 (multi-agent coordination)
772
- * See: CLAUDE.md §2.2
773
- */
774
- async function _ensureCleanWorkingTree() {
775
- const status = await getGitForCwd().getStatus();
776
- if (status.trim()) {
777
- die(`Working tree is not clean. Cannot proceed with wu:done.\n\n` +
778
- `Uncommitted changes in main checkout:\n${status}\n\n` +
779
- `⚠️ CRITICAL: These may be another agent's work!\n\n` +
780
- `Before proceeding:\n` +
781
- `1. Check if these are YOUR changes (forgot to commit in main)\n` +
782
- ` → If yes: Commit them now, then retry wu:done\n\n` +
783
- `2. Check if these are ANOTHER AGENT's changes\n` +
784
- ` → If yes: STOP. Coordinate with user before proceeding\n` +
785
- ` → NEVER remove another agent's uncommitted work\n\n` +
786
- `Multi-agent coordination: See CLAUDE.md §2.2\n\n` +
787
- `Common causes:\n` +
788
- ` - You forgot to commit changes before claiming a different WU\n` +
789
- ` - Another agent is actively working in main checkout\n` +
790
- ` - Leftover changes from previous session`);
791
- }
792
- }
793
- /**
794
- * Extract completed WU IDs from git log output.
795
- * @param {string} logOutput - Git log output (one commit per line)
796
- * @param {string} currentId - Current WU ID to exclude
797
- * @returns {string[]} Array of completed WU IDs
798
- */
799
- function extractCompletedWUIds(logOutput, currentId) {
800
- const wuPattern = /wu\((wu-\d+)\):/gi;
801
- const seenIds = new Set();
802
- const completedWUs = [];
803
- for (const line of logOutput.split(STRING_LITERALS.NEWLINE)) {
804
- // Only process "done" commits
805
- if (!line.toLowerCase().includes('done'))
806
- continue;
807
- let match;
808
- while ((match = wuPattern.exec(line)) !== null) {
809
- const wuId = match[1].toUpperCase();
810
- // Skip current WU and duplicates
811
- if (wuId !== currentId && !seenIds.has(wuId)) {
812
- seenIds.add(wuId);
813
- completedWUs.push(wuId);
814
- }
815
- }
816
- }
817
- return completedWUs;
818
- }
819
- /**
820
- * Build warning message for parallel completions.
821
- */
822
- function buildParallelWarning(id, completedWUs, baselineSha, currentSha) {
823
- const wuList = completedWUs.map((wu) => ` • ${wu}`).join(STRING_LITERALS.NEWLINE);
824
- return `
825
- ${EMOJI.WARNING} PARALLEL COMPLETIONS DETECTED ${EMOJI.WARNING}
826
-
827
- The following WUs were completed and merged to main since you claimed ${id}:
828
-
829
- ${wuList}
830
-
831
- This may cause rebase conflicts when wu:done attempts to merge.
832
-
833
- Options:
834
- 1. Proceed anyway - rebase will attempt to resolve conflicts
835
- 2. Abort and manually rebase: git fetch origin main && git rebase origin/main
836
- 3. Check if other completed WUs touched the same files
837
-
838
- Baseline: ${baselineSha.substring(0, 8)}
839
- Current: ${currentSha.substring(0, 8)}
840
- `;
841
- }
842
- /**
843
- * WU-1382: Detect parallel WU completions since claim time.
844
- *
845
- * When multiple agents work in parallel, one may complete a WU and merge to main
846
- * while another is still working. This function detects such completions early,
847
- * before wu:done attempts the merge, allowing the agent to decide whether to
848
- * proceed (with potential rebase conflicts) or abort.
849
- *
850
- * @param {string} id - Current WU ID
851
- * @param {object} doc - WU YAML document (from worktree or main)
852
- * @returns {Promise<{hasParallelCompletions: boolean, completedWUs: string[], warning: string|null}>}
853
- */
854
- async function detectParallelCompletions(id, doc) {
855
- const noParallel = {
856
- hasParallelCompletions: false,
857
- completedWUs: [],
858
- warning: null,
859
- };
860
- const baselineSha = doc.baseline_main_sha;
861
- // If no baseline recorded (legacy WU), skip detection
862
- if (!baselineSha) {
863
- console.log(`${LOG_PREFIX.DONE} ${EMOJI.INFO} No baseline_main_sha recorded (legacy WU) - skipping parallel detection`);
864
- return noParallel;
865
- }
866
- try {
867
- const gitAdapter = getGitForCwd();
868
- await gitAdapter.fetch(REMOTES.ORIGIN, BRANCHES.MAIN);
869
- const currentSha = (await gitAdapter.getCommitHash(`${REMOTES.ORIGIN}/${BRANCHES.MAIN}`)).trim();
870
- if (currentSha === baselineSha) {
871
- console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} No parallel completions detected (main unchanged since claim)`);
872
- return noParallel;
873
- }
874
- const logOutput = await gitAdapter.raw([
875
- 'log',
876
- '--oneline',
877
- '--grep=^wu(wu-',
878
- `${baselineSha}..${REMOTES.ORIGIN}/${BRANCHES.MAIN}`,
879
- ]);
880
- if (!logOutput?.trim()) {
881
- console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Main advanced since claim but no WU completions detected`);
882
- return noParallel;
883
- }
884
- const completedWUs = extractCompletedWUIds(logOutput, id);
885
- if (completedWUs.length === 0) {
886
- console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Main advanced since claim but no other WU completions`);
887
- return noParallel;
888
- }
889
- const warning = buildParallelWarning(id, completedWUs, baselineSha, currentSha);
890
- return { hasParallelCompletions: true, completedWUs, warning };
891
- }
892
- catch (err) {
893
- console.warn(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Could not detect parallel completions: ${getErrorMessage(err)}`);
894
- return noParallel;
895
- }
896
- }
897
- /**
898
- * Ensure main branch is up-to-date with origin before merge operations.
899
- *
900
- * Prevents coordination failures when Agent A pushes to main while Agent B
901
- * is working. Without this check, Agent B's wu:done would fail with cryptic
902
- * fast-forward errors when trying to merge.
903
- *
904
- * Context: WU-705 (fix agent coordination failures)
905
- * See: CLAUDE.md §2.7
906
- */
907
- async function ensureMainUpToDate() {
908
- console.log(`${LOG_PREFIX.DONE} Checking if main is up-to-date with origin...`);
909
- try {
910
- // Fetch latest without merging
911
- const gitAdapter = getGitForCwd();
912
- await gitAdapter.fetch(REMOTES.ORIGIN, BRANCHES.MAIN);
913
- const localMain = await gitAdapter.getCommitHash(BRANCHES.MAIN);
914
- const remoteMain = await gitAdapter.getCommitHash(`${REMOTES.ORIGIN}/${BRANCHES.MAIN}`);
915
- if (localMain !== remoteMain) {
916
- const behind = await gitAdapter.revList([
917
- '--count',
918
- `${BRANCHES.MAIN}..${REMOTES.ORIGIN}/${BRANCHES.MAIN}`,
919
- ]);
920
- const ahead = await gitAdapter.revList([
921
- '--count',
922
- `${REMOTES.ORIGIN}/${BRANCHES.MAIN}..${BRANCHES.MAIN}`,
923
- ]);
924
- die(`Main branch is out of sync with ${REMOTES.ORIGIN}.\n\n` +
925
- `Local ${BRANCHES.MAIN} is ${behind} commits behind and ${ahead} commits ahead of ${REMOTES.ORIGIN}/${BRANCHES.MAIN}.\n\n` +
926
- `Update main before running wu:done:\n` +
927
- ` git pull origin main\n` +
928
- ` # Then retry:\n` +
929
- ` pnpm wu:done --id ${process.argv.find((a) => a.startsWith('WU-')) || 'WU-XXX'}\n\n` +
930
- `This prevents fast-forward merge failures during wu:done completion.\n\n` +
931
- `Why this happens:\n` +
932
- ` - Another agent completed a WU and pushed to main\n` +
933
- ` - Your main checkout is now behind origin/main\n` +
934
- ` - The fast-forward merge will fail without updating first\n\n` +
935
- `Multi-agent coordination: See CLAUDE.md §2.7`);
936
- }
937
- console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Main is up-to-date with origin`);
938
- }
939
- catch (err) {
940
- console.warn(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Could not verify main sync: ${getErrorMessage(err)}`);
941
- console.warn(`${LOG_PREFIX.DONE} Proceeding anyway (network issue or no remote)`);
942
- }
943
- }
944
- /**
945
- * Tripwire check: Scan commands log for violations (WU-630 detective layer)
946
- *
947
- * Scans .lumenflow/commands.log for destructive git commands executed during
948
- * this agent session. If violations are found, aborts wu:done and displays
949
- * remediation guidance.
950
- *
951
- * This is defense-in-depth: catches violations even if git shim was bypassed
952
- * by calling /usr/bin/git directly or if PATH was not set up correctly.
953
- *
954
- * Context: WU-630 (detective layer, Layer 3 of 4)
955
- * See: https://lumenflow.dev/reference/playbook/ §4.6
956
- */
957
- function runTripwireCheck() {
958
- const violations = scanLogForViolations();
959
- if (violations.length === 0) {
960
- return; // All clear
961
- }
962
- // Violations detected - format error message with remediation
963
- console.error('\n⛔ VIOLATION DETECTED: Destructive Git Commands on Main\n');
964
- console.error('The following forbidden git commands were executed during this session:\n');
965
- violations.forEach((v, i) => {
966
- console.error(` ${i + 1}. ${v.command}`);
967
- console.error(` Branch: ${v.branch}`);
968
- console.error(` Worktree: ${v.worktree}`);
969
- console.error(` Time: ${v.timestamp}\n`);
970
- });
971
- console.error(`\nTotal: ${violations.length} violations\n`);
972
- // Remediation guidance based on violation type
973
- console.error("⚠️ CRITICAL: These commands may have destroyed other agents' work!\n");
974
- console.error('Remediation Steps:\n');
975
- const hasReset = violations.some((v) => v.command.includes('reset --hard'));
976
- const hasStash = violations.some((v) => v.command.includes('stash'));
977
- const hasClean = violations.some((v) => v.command.includes('clean'));
978
- if (hasReset) {
979
- console.error('📋 git reset --hard detected:');
980
- console.error(' 1. Check git reflog to recover lost commits:');
981
- console.error(' git reflog');
982
- console.error(' git reset --hard HEAD@{N} (where N is the commit before reset)');
983
- console.error(' 2. If reflog shows lost work, restore it immediately\n');
984
- }
985
- if (hasStash) {
986
- console.error('📋 git stash detected:');
987
- console.error(" 1. Check if stash contains other agents' work:");
988
- console.error(' git stash list');
989
- console.error(' git stash show -p stash@{0}');
990
- console.error(' 2. If stash contains work, pop it back:');
991
- console.error(' git stash pop\n');
992
- }
993
- if (hasClean) {
994
- console.error('📋 git clean detected:');
995
- console.error(' 1. Deleted files may not be recoverable');
996
- console.error(' 2. Check git status for remaining untracked files');
997
- console.error(' 3. Escalate to human if critical files were deleted\n');
998
- }
999
- console.error('📖 See detailed recovery steps:');
1000
- console.error(' https://lumenflow.dev/reference/playbook/ §4.6\n');
1001
- console.error('🚫 DO NOT proceed with wu:done until violations are remediated.\n');
1002
- console.error('Fix violations first, then retry wu:done.\n');
1003
- // Also rotate log (cleanup old entries)
1004
- rotateLog();
1005
- process.exit(EXIT_CODES.ERROR);
1006
- }
1007
- async function listStaged(gitAdapter) {
1008
- // WU-1541: Use explicit gitAdapter if provided, otherwise fall back to getGitForCwd()
1009
- // WU-1235: getGitForCwd() captures current directory (legacy behavior)
1010
- const gitCwd = gitAdapter ?? getGitForCwd();
1011
- const raw = await gitCwd.raw(['diff', '--cached', '--name-only']);
1012
- return raw ? raw.split(/\r?\n/).filter(Boolean) : [];
1013
- }
1014
- // In --no-auto mode, allow a safe no-op: if NONE of the expected files are staged,
1015
- // treat as already-synchronised and continue. If SOME are staged and SOME missing,
1016
- // still fail with guidance.
1017
- async function ensureNoAutoStagedOrNoop(paths) {
1018
- const staged = await listStaged();
1019
- const isStaged = (p) => staged.some((name) => name === p || name.startsWith(`${p}/`));
1020
- const definedPaths = paths.filter((p) => typeof p === 'string' && p.length > 0);
1021
- const present = definedPaths.filter((p) => isStaged(p));
1022
- if (present.length === 0) {
1023
- console.log(`${LOG_PREFIX.DONE} No staged changes detected for --no-auto; treating as no-op finalisation (repo already in done state)`);
1024
- return { noop: true };
1025
- }
1026
- const missing = definedPaths.filter((p) => !isStaged(p));
1027
- if (missing.length > 0) {
1028
- die(`Stage updates for: ${missing.join(', ')}`);
1029
- }
1030
- return { noop: false };
1031
- }
1032
480
  export function emitTelemetry(event) {
1033
481
  const logPath = path.join('.lumenflow', 'flow.log');
1034
482
  const logDir = path.dirname(logPath);
@@ -1201,56 +649,6 @@ async function runGatesInWorktree(worktreePath, id, options = {}) {
1201
649
  die(`Gates failed in ${worktreePath}. Fix issues in the worktree and try again.`);
1202
650
  }
1203
651
  }
1204
- async function validateStagedFiles(id, isDocsOnly = false, gitAdapter, options = {}) {
1205
- // WU-1541: Accept optional gitAdapter to avoid process.chdir dependency
1206
- const staged = await listStaged(gitAdapter);
1207
- // WU-1311: Use config-based paths instead of hardcoded docs-layout paths
1208
- const config = getConfig();
1209
- const wuPath = `${config.directories.wuDir}/${id}.yaml`;
1210
- // WU-1740: Include wu-events.jsonl to persist state store events
1211
- const whitelist = [
1212
- wuPath,
1213
- config.directories.statusPath,
1214
- config.directories.backlogPath,
1215
- resolveWuEventsRelativePath(process.cwd()),
1216
- ];
1217
- const metadataAllowlist = (options.metadataAllowlist ?? []).filter((file) => typeof file === 'string' && file.length > 0);
1218
- const whitelistSet = new Set([...whitelist, ...metadataAllowlist]);
1219
- if (isDocsOnly) {
1220
- // For docs-only WUs, validate that all staged files are in allowed paths
1221
- const docsResult = validateDocsOnly(staged);
1222
- if (!docsResult.valid) {
1223
- die(`Docs-only WU cannot modify code files:\n ${docsResult.violations.join(`${STRING_LITERALS.NEWLINE} `)}\n\n${getAllowedPathsDescription()}`);
1224
- }
1225
- console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Docs-only path validation passed`);
1226
- return;
1227
- }
1228
- const unexpected = staged.filter((file) => {
1229
- // Whitelist exact matches
1230
- if (whitelistSet.has(file))
1231
- return false;
1232
- // Whitelist stamps directory pattern
1233
- if (file.startsWith(`${LUMENFLOW_PATHS.STAMPS_DIR}/`))
1234
- return false;
1235
- // WU-1072: Whitelist apps/docs/**/*.mdx for auto-generated docs from turbo docs:generate
1236
- if (file.startsWith('apps/docs/') && file.endsWith('.mdx'))
1237
- return false;
1238
- return true;
1239
- });
1240
- if (unexpected.length > 0) {
1241
- // WU-1311: Use config-based pattern for WU YAML detection
1242
- const wuDirPattern = config.directories.wuDir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1243
- // eslint-disable-next-line security/detect-non-literal-regexp -- config path escaped for regex; not user input
1244
- const wuYamlRegex = new RegExp(`^${wuDirPattern}/WU-\\d+\\.yaml$`);
1245
- const otherWuYamlOnly = unexpected.every((f) => wuYamlRegex.test(f));
1246
- if (otherWuYamlOnly) {
1247
- console.warn(`${LOG_PREFIX.DONE} Warning: other WU YAMLs are staged; proceeding and committing only current WU files.`);
1248
- }
1249
- else {
1250
- die(`Unexpected files staged (only current WU metadata, current parent initiative YAML, and .lumenflow/stamps/<id>.done allowed):\n ${unexpected.join(`${STRING_LITERALS.NEWLINE} `)}`);
1251
- }
1252
- }
1253
- }
1254
652
  // Note: updateStatusRemoveInProgress, addToStatusCompleted, and moveWUToDoneBacklog
1255
653
  // have been extracted to tools/lib/wu-status-updater.ts and imported above (WU-1163)
1256
654
  //
@@ -1259,39 +657,6 @@ async function validateStagedFiles(id, isDocsOnly = false, gitAdapter, options =
1259
657
  // Note: readWUPreferWorktree, detectCurrentWorktree, defaultWorktreeFrom, detectWorkspaceMode,
1260
658
  // defaultBranchFrom, branchExists, runCleanup have been extracted to
1261
659
  // tools/lib/wu-done-validators.ts and imported above (WU-1215)
1262
- /**
1263
- * Validate Branch-Only mode requirements before proceeding
1264
- * @param {string} laneBranch - Expected lane branch name
1265
- * @returns {{valid: boolean, error: string|null}}
1266
- */
1267
- async function validateBranchOnlyMode(laneBranch) {
1268
- // Check we're on the correct lane branch
1269
- const gitAdapter = getGitForCwd();
1270
- const currentBranch = await gitAdapter.getCurrentBranch();
1271
- if (currentBranch !== laneBranch) {
1272
- return {
1273
- valid: false,
1274
- error: `Branch-Only mode error: Not on the lane branch.\n\n` +
1275
- `Expected branch: ${laneBranch}\n` +
1276
- `Current branch: ${currentBranch}\n\n` +
1277
- `Fix: git checkout ${laneBranch}`,
1278
- };
1279
- }
1280
- // Check working directory is clean
1281
- const status = await gitAdapter.getStatus();
1282
- if (status) {
1283
- return {
1284
- valid: false,
1285
- error: `Branch-Only mode error: Working directory is not clean.\n\n` +
1286
- `Uncommitted changes detected:\n${status}\n\n` +
1287
- `Fix: Commit all changes before running wu:done\n` +
1288
- ` git add -A\n` +
1289
- ` git commit -m "wu(wu-xxx): ..."\n` +
1290
- ` git push origin ${laneBranch}`,
1291
- };
1292
- }
1293
- return { valid: true, error: null };
1294
- }
1295
660
  /**
1296
661
  * WU-755 + WU-1230: Record transaction state for rollback
1297
662
  * @param {string} id - WU ID
@@ -1757,8 +1122,6 @@ async function executePreFlightChecks({ id, args, isBranchOnly, isDocsOnly, docM
1757
1122
  else {
1758
1123
  // Worktree mode: must be on main
1759
1124
  await ensureOnMain(getGitForCwd());
1760
- // Prevent coordination failures by ensuring main is up-to-date
1761
- await ensureMainUpToDate();
1762
1125
  // P0 EMERGENCY FIX Part 1: Restore wu-events.jsonl BEFORE parallel completion check
1763
1126
  // Previous wu:done runs or memory layer writes may have left this file dirty,
1764
1127
  // which causes the auto-rebase to fail with "You have unstaged changes"
@@ -2233,6 +1596,62 @@ export async function main() {
2233
1596
  const initialDocForValidation = normalizeWUDocLike(initialDocForValidationRaw);
2234
1597
  // Capture main checkout path once. process.cwd() may drift later during recovery flows.
2235
1598
  const mainCheckoutPath = process.cwd();
1599
+ // ──────────────────────────────────────────────
1600
+ // WU-2211: --already-merged early exit path
1601
+ // Skips merge phase, gates, worktree detection. Only writes metadata.
1602
+ // ──────────────────────────────────────────────
1603
+ if (args.alreadyMerged) {
1604
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.INFO} WU-2211: --already-merged mode activated`);
1605
+ // Safety check: verify code_paths exist on HEAD of main
1606
+ const codePaths = docMain.code_paths || [];
1607
+ const verification = await verifyCodePathsOnMainHead(codePaths);
1608
+ if (!verification.valid) {
1609
+ die(`${EMOJI.FAILURE} --already-merged safety check failed\n\n` +
1610
+ `${verification.error}\n\n` +
1611
+ `Cannot finalize ${id}: code_paths must exist on HEAD before using --already-merged.`);
1612
+ }
1613
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Safety check passed: all ${codePaths.length} code_paths verified on HEAD`);
1614
+ // Execute finalize-only path
1615
+ const title = String(docMain.title || id);
1616
+ const lane = String(docMain.lane || '');
1617
+ const finalizeResult = await executeAlreadyMergedFinalizeFromModule({
1618
+ id,
1619
+ title,
1620
+ lane,
1621
+ doc: docMain,
1622
+ });
1623
+ if (!finalizeResult.success) {
1624
+ die(`${EMOJI.FAILURE} --already-merged finalization failed\n\n` +
1625
+ `Errors:\n${finalizeResult.errors.map((e) => ` - ${e}`).join('\n')}\n\n` +
1626
+ `Partial state may remain. Rerun: pnpm wu:done --id ${id} --already-merged`);
1627
+ }
1628
+ // Release lane lock (non-blocking, same as normal wu:done)
1629
+ try {
1630
+ const lane = docMain.lane;
1631
+ if (lane) {
1632
+ const releaseResult = releaseLaneLock(lane, { wuId: id });
1633
+ if (releaseResult.released && !releaseResult.notFound) {
1634
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Lane lock released for "${lane}"`);
1635
+ }
1636
+ }
1637
+ }
1638
+ catch (err) {
1639
+ console.warn(`${LOG_PREFIX.DONE} Warning: Could not release lane lock: ${getErrorMessage(err)}`);
1640
+ }
1641
+ // End agent session (non-blocking)
1642
+ try {
1643
+ endSessionForWU();
1644
+ }
1645
+ catch {
1646
+ // Non-blocking
1647
+ }
1648
+ // Broadcast completion signal (non-blocking)
1649
+ await broadcastCompletionSignal(id, title);
1650
+ console.log(`\n${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} ${id} finalized via --already-merged`);
1651
+ console.log(`- WU: ${id} -- ${title}`);
1652
+ clearConfigCache();
1653
+ process.exit(EXIT_CODES.SUCCESS);
1654
+ }
2236
1655
  // WU-1663: Determine prepPassed early for pipeline actor input.
2237
1656
  // canSkipGates checks if wu:prep already ran gates successfully via checkpoint.
2238
1657
  // This drives the isPrepPassed guard on the GATES_SKIPPED transition.