@lumenflow/core 1.0.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/LICENSE +190 -0
- package/README.md +119 -0
- package/dist/active-wu-detector.d.ts +33 -0
- package/dist/active-wu-detector.js +106 -0
- package/dist/adapters/filesystem-metrics.adapter.d.ts +108 -0
- package/dist/adapters/filesystem-metrics.adapter.js +519 -0
- package/dist/adapters/terminal-renderer.adapter.d.ts +106 -0
- package/dist/adapters/terminal-renderer.adapter.js +337 -0
- package/dist/arg-parser.d.ts +63 -0
- package/dist/arg-parser.js +560 -0
- package/dist/backlog-editor.d.ts +98 -0
- package/dist/backlog-editor.js +179 -0
- package/dist/backlog-generator.d.ts +111 -0
- package/dist/backlog-generator.js +381 -0
- package/dist/backlog-parser.d.ts +45 -0
- package/dist/backlog-parser.js +102 -0
- package/dist/backlog-sync-validator.d.ts +78 -0
- package/dist/backlog-sync-validator.js +294 -0
- package/dist/branch-drift.d.ts +34 -0
- package/dist/branch-drift.js +51 -0
- package/dist/cleanup-install-config.d.ts +33 -0
- package/dist/cleanup-install-config.js +37 -0
- package/dist/cleanup-lock.d.ts +139 -0
- package/dist/cleanup-lock.js +313 -0
- package/dist/code-path-validator.d.ts +146 -0
- package/dist/code-path-validator.js +537 -0
- package/dist/code-paths-overlap.d.ts +55 -0
- package/dist/code-paths-overlap.js +245 -0
- package/dist/commands-logger.d.ts +77 -0
- package/dist/commands-logger.js +254 -0
- package/dist/commit-message-utils.d.ts +25 -0
- package/dist/commit-message-utils.js +41 -0
- package/dist/compliance-parser.d.ts +150 -0
- package/dist/compliance-parser.js +507 -0
- package/dist/constants/backlog-patterns.d.ts +20 -0
- package/dist/constants/backlog-patterns.js +23 -0
- package/dist/constants/dora-constants.d.ts +49 -0
- package/dist/constants/dora-constants.js +53 -0
- package/dist/constants/gate-constants.d.ts +15 -0
- package/dist/constants/gate-constants.js +15 -0
- package/dist/constants/linter-constants.d.ts +16 -0
- package/dist/constants/linter-constants.js +16 -0
- package/dist/constants/tokenizer-constants.d.ts +15 -0
- package/dist/constants/tokenizer-constants.js +15 -0
- package/dist/core/scope-checker.d.ts +97 -0
- package/dist/core/scope-checker.js +163 -0
- package/dist/core/tool-runner.d.ts +161 -0
- package/dist/core/tool-runner.js +393 -0
- package/dist/core/tool.constants.d.ts +105 -0
- package/dist/core/tool.constants.js +101 -0
- package/dist/core/tool.schemas.d.ts +226 -0
- package/dist/core/tool.schemas.js +226 -0
- package/dist/core/worktree-guard.d.ts +130 -0
- package/dist/core/worktree-guard.js +242 -0
- package/dist/coverage-gate.d.ts +108 -0
- package/dist/coverage-gate.js +196 -0
- package/dist/date-utils.d.ts +75 -0
- package/dist/date-utils.js +140 -0
- package/dist/dependency-graph.d.ts +142 -0
- package/dist/dependency-graph.js +550 -0
- package/dist/dependency-guard.d.ts +54 -0
- package/dist/dependency-guard.js +142 -0
- package/dist/dependency-validator.d.ts +105 -0
- package/dist/dependency-validator.js +154 -0
- package/dist/docs-path-validator.d.ts +36 -0
- package/dist/docs-path-validator.js +95 -0
- package/dist/domain/orchestration.constants.d.ts +99 -0
- package/dist/domain/orchestration.constants.js +97 -0
- package/dist/domain/orchestration.schemas.d.ts +280 -0
- package/dist/domain/orchestration.schemas.js +211 -0
- package/dist/domain/orchestration.types.d.ts +133 -0
- package/dist/domain/orchestration.types.js +12 -0
- package/dist/error-handler.d.ts +116 -0
- package/dist/error-handler.js +136 -0
- package/dist/file-classifiers.d.ts +62 -0
- package/dist/file-classifiers.js +108 -0
- package/dist/gates-agent-mode.d.ts +81 -0
- package/dist/gates-agent-mode.js +94 -0
- package/dist/generate-traceability.d.ts +107 -0
- package/dist/generate-traceability.js +411 -0
- package/dist/git-adapter.d.ts +395 -0
- package/dist/git-adapter.js +649 -0
- package/dist/git-staged-validator.d.ts +32 -0
- package/dist/git-staged-validator.js +48 -0
- package/dist/hardcoded-strings.d.ts +61 -0
- package/dist/hardcoded-strings.js +270 -0
- package/dist/incremental-lint.d.ts +78 -0
- package/dist/incremental-lint.js +129 -0
- package/dist/incremental-test.d.ts +39 -0
- package/dist/incremental-test.js +61 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +61 -0
- package/dist/invariants/check-automated-tests.d.ts +50 -0
- package/dist/invariants/check-automated-tests.js +166 -0
- package/dist/invariants-runner.d.ts +103 -0
- package/dist/invariants-runner.js +527 -0
- package/dist/lane-checker.d.ts +50 -0
- package/dist/lane-checker.js +319 -0
- package/dist/lane-inference.d.ts +39 -0
- package/dist/lane-inference.js +195 -0
- package/dist/lane-lock.d.ts +211 -0
- package/dist/lane-lock.js +474 -0
- package/dist/lane-validator.d.ts +48 -0
- package/dist/lane-validator.js +114 -0
- package/dist/logs-lib.d.ts +104 -0
- package/dist/logs-lib.js +207 -0
- package/dist/lumenflow-config-schema.d.ts +272 -0
- package/dist/lumenflow-config-schema.js +207 -0
- package/dist/lumenflow-config.d.ts +95 -0
- package/dist/lumenflow-config.js +236 -0
- package/dist/manual-test-validator.d.ts +80 -0
- package/dist/manual-test-validator.js +200 -0
- package/dist/merge-lock.d.ts +115 -0
- package/dist/merge-lock.js +251 -0
- package/dist/micro-worktree.d.ts +159 -0
- package/dist/micro-worktree.js +427 -0
- package/dist/migration-deployer.d.ts +69 -0
- package/dist/migration-deployer.js +151 -0
- package/dist/orchestration-advisory-loader.d.ts +28 -0
- package/dist/orchestration-advisory-loader.js +87 -0
- package/dist/orchestration-advisory.d.ts +58 -0
- package/dist/orchestration-advisory.js +94 -0
- package/dist/orchestration-di.d.ts +48 -0
- package/dist/orchestration-di.js +57 -0
- package/dist/orchestration-rules.d.ts +57 -0
- package/dist/orchestration-rules.js +201 -0
- package/dist/orphan-detector.d.ts +131 -0
- package/dist/orphan-detector.js +226 -0
- package/dist/path-classifiers.d.ts +57 -0
- package/dist/path-classifiers.js +93 -0
- package/dist/piped-command-detector.d.ts +34 -0
- package/dist/piped-command-detector.js +64 -0
- package/dist/ports/dashboard-renderer.port.d.ts +112 -0
- package/dist/ports/dashboard-renderer.port.js +25 -0
- package/dist/ports/metrics-collector.port.d.ts +132 -0
- package/dist/ports/metrics-collector.port.js +26 -0
- package/dist/process-detector.d.ts +84 -0
- package/dist/process-detector.js +172 -0
- package/dist/prompt-linter.d.ts +72 -0
- package/dist/prompt-linter.js +312 -0
- package/dist/prompt-monitor.d.ts +15 -0
- package/dist/prompt-monitor.js +205 -0
- package/dist/rebase-artifact-cleanup.d.ts +145 -0
- package/dist/rebase-artifact-cleanup.js +433 -0
- package/dist/retry-strategy.d.ts +189 -0
- package/dist/retry-strategy.js +283 -0
- package/dist/risk-detector.d.ts +108 -0
- package/dist/risk-detector.js +252 -0
- package/dist/rollback-utils.d.ts +76 -0
- package/dist/rollback-utils.js +104 -0
- package/dist/section-headings.d.ts +43 -0
- package/dist/section-headings.js +49 -0
- package/dist/spawn-escalation.d.ts +90 -0
- package/dist/spawn-escalation.js +253 -0
- package/dist/spawn-monitor.d.ts +229 -0
- package/dist/spawn-monitor.js +672 -0
- package/dist/spawn-recovery.d.ts +82 -0
- package/dist/spawn-recovery.js +298 -0
- package/dist/spawn-registry-schema.d.ts +98 -0
- package/dist/spawn-registry-schema.js +108 -0
- package/dist/spawn-registry-store.d.ts +146 -0
- package/dist/spawn-registry-store.js +273 -0
- package/dist/spawn-tree.d.ts +121 -0
- package/dist/spawn-tree.js +285 -0
- package/dist/stamp-status-validator.d.ts +84 -0
- package/dist/stamp-status-validator.js +134 -0
- package/dist/stamp-utils.d.ts +100 -0
- package/dist/stamp-utils.js +229 -0
- package/dist/state-machine.d.ts +26 -0
- package/dist/state-machine.js +83 -0
- package/dist/system-map-validator.d.ts +80 -0
- package/dist/system-map-validator.js +272 -0
- package/dist/telemetry.d.ts +80 -0
- package/dist/telemetry.js +213 -0
- package/dist/token-counter.d.ts +51 -0
- package/dist/token-counter.js +145 -0
- package/dist/usecases/get-dashboard-data.usecase.d.ts +52 -0
- package/dist/usecases/get-dashboard-data.usecase.js +61 -0
- package/dist/usecases/get-suggestions.usecase.d.ts +100 -0
- package/dist/usecases/get-suggestions.usecase.js +153 -0
- package/dist/user-normalizer.d.ts +41 -0
- package/dist/user-normalizer.js +141 -0
- package/dist/validators/phi-constants.d.ts +97 -0
- package/dist/validators/phi-constants.js +152 -0
- package/dist/validators/phi-scanner.d.ts +58 -0
- package/dist/validators/phi-scanner.js +215 -0
- package/dist/worktree-ownership.d.ts +50 -0
- package/dist/worktree-ownership.js +74 -0
- package/dist/worktree-scanner.d.ts +103 -0
- package/dist/worktree-scanner.js +168 -0
- package/dist/worktree-symlink.d.ts +99 -0
- package/dist/worktree-symlink.js +359 -0
- package/dist/wu-backlog-updater.d.ts +17 -0
- package/dist/wu-backlog-updater.js +37 -0
- package/dist/wu-checkpoint.d.ts +124 -0
- package/dist/wu-checkpoint.js +233 -0
- package/dist/wu-claim-helpers.d.ts +26 -0
- package/dist/wu-claim-helpers.js +63 -0
- package/dist/wu-claim-resume.d.ts +106 -0
- package/dist/wu-claim-resume.js +276 -0
- package/dist/wu-consistency-checker.d.ts +95 -0
- package/dist/wu-consistency-checker.js +567 -0
- package/dist/wu-constants.d.ts +1275 -0
- package/dist/wu-constants.js +1382 -0
- package/dist/wu-create-validators.d.ts +42 -0
- package/dist/wu-create-validators.js +93 -0
- package/dist/wu-done-branch-only.d.ts +63 -0
- package/dist/wu-done-branch-only.js +191 -0
- package/dist/wu-done-messages.d.ts +119 -0
- package/dist/wu-done-messages.js +185 -0
- package/dist/wu-done-pr.d.ts +72 -0
- package/dist/wu-done-pr.js +174 -0
- package/dist/wu-done-retry-helpers.d.ts +85 -0
- package/dist/wu-done-retry-helpers.js +172 -0
- package/dist/wu-done-ui.d.ts +37 -0
- package/dist/wu-done-ui.js +69 -0
- package/dist/wu-done-validators.d.ts +411 -0
- package/dist/wu-done-validators.js +1229 -0
- package/dist/wu-done-worktree.d.ts +182 -0
- package/dist/wu-done-worktree.js +1097 -0
- package/dist/wu-helpers.d.ts +128 -0
- package/dist/wu-helpers.js +248 -0
- package/dist/wu-lint.d.ts +70 -0
- package/dist/wu-lint.js +234 -0
- package/dist/wu-paths.d.ts +171 -0
- package/dist/wu-paths.js +178 -0
- package/dist/wu-preflight-validators.d.ts +86 -0
- package/dist/wu-preflight-validators.js +251 -0
- package/dist/wu-recovery.d.ts +138 -0
- package/dist/wu-recovery.js +341 -0
- package/dist/wu-repair-core.d.ts +131 -0
- package/dist/wu-repair-core.js +669 -0
- package/dist/wu-schema-normalization.d.ts +17 -0
- package/dist/wu-schema-normalization.js +82 -0
- package/dist/wu-schema.d.ts +793 -0
- package/dist/wu-schema.js +881 -0
- package/dist/wu-spawn-helpers.d.ts +121 -0
- package/dist/wu-spawn-helpers.js +271 -0
- package/dist/wu-spawn.d.ts +158 -0
- package/dist/wu-spawn.js +1306 -0
- package/dist/wu-state-schema.d.ts +213 -0
- package/dist/wu-state-schema.js +156 -0
- package/dist/wu-state-store.d.ts +264 -0
- package/dist/wu-state-store.js +691 -0
- package/dist/wu-status-transition.d.ts +63 -0
- package/dist/wu-status-transition.js +382 -0
- package/dist/wu-status-updater.d.ts +25 -0
- package/dist/wu-status-updater.js +116 -0
- package/dist/wu-transaction-collectors.d.ts +116 -0
- package/dist/wu-transaction-collectors.js +272 -0
- package/dist/wu-transaction.d.ts +170 -0
- package/dist/wu-transaction.js +273 -0
- package/dist/wu-validation-constants.d.ts +60 -0
- package/dist/wu-validation-constants.js +66 -0
- package/dist/wu-validation.d.ts +118 -0
- package/dist/wu-validation.js +243 -0
- package/dist/wu-validator.d.ts +62 -0
- package/dist/wu-validator.js +325 -0
- package/dist/wu-yaml-fixer.d.ts +97 -0
- package/dist/wu-yaml-fixer.js +264 -0
- package/dist/wu-yaml.d.ts +86 -0
- package/dist/wu-yaml.js +222 -0
- package/package.json +114 -0
|
@@ -0,0 +1,1097 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Worktree mode completion workflow for wu:done
|
|
4
|
+
* Extracted from wu-done.mjs (WU-1215 refactoring)
|
|
5
|
+
* Updated in WU-1369 to use atomic transaction pattern.
|
|
6
|
+
*
|
|
7
|
+
* Flow (WU-1369 Atomic Pattern):
|
|
8
|
+
* 1. cd into worktree
|
|
9
|
+
* 2. Read and validate WU state
|
|
10
|
+
* 3. Run ALL validations FIRST (before any file writes)
|
|
11
|
+
* 4. Collect all metadata changes into transaction (in memory)
|
|
12
|
+
* 5. Commit transaction (atomic write)
|
|
13
|
+
* 6. Stage and format files
|
|
14
|
+
* 7. Git commit in worktree
|
|
15
|
+
* 8. Return to main checkout
|
|
16
|
+
* 9. Either merge (default) OR create PR (pr-mode)
|
|
17
|
+
* 10. Push to origin
|
|
18
|
+
*
|
|
19
|
+
* Key guarantee: If any validation fails, NO files are modified.
|
|
20
|
+
*/
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
import { writeFile } from 'node:fs/promises';
|
|
23
|
+
import { generateCommitMessage, collectMetadataToTransaction, stageAndFormatMetadata, defaultBranchFrom, branchExists, validatePostMutation, } from './wu-done-validators.js';
|
|
24
|
+
import { getGitForCwd } from './git-adapter.js';
|
|
25
|
+
import { readWU, writeWU } from './wu-yaml.js';
|
|
26
|
+
import { WU_PATHS } from './wu-paths.js';
|
|
27
|
+
import { BRANCHES, REMOTES, THRESHOLDS, LOG_PREFIX, EMOJI, COMMIT_FORMATS, BOX, STRING_LITERALS, WU_STATUS, GIT_COMMANDS, GIT_FLAGS, } from './wu-constants.js';
|
|
28
|
+
import { RECOVERY, REBASE, PREFLIGHT, MERGE } from './wu-done-messages.js';
|
|
29
|
+
import { getDriftLevel, DRIFT_LEVELS } from './branch-drift.js';
|
|
30
|
+
import { createError, ErrorCodes } from './error-handler.js';
|
|
31
|
+
import { validateDoneWU, validateAndNormalizeWUYAML } from './wu-schema.js';
|
|
32
|
+
import { assertTransition } from './state-machine.js';
|
|
33
|
+
import { detectZombieState, resetWorktreeYAMLForRecovery, getRecoveryAttemptCount, incrementRecoveryAttempt, clearRecoveryAttempts, shouldEscalateToManualIntervention, MAX_RECOVERY_ATTEMPTS, } from './wu-recovery.js';
|
|
34
|
+
import { isPRModeEnabled, createPR, printPRCreatedMessage } from './wu-done-pr.js';
|
|
35
|
+
// WU-1371: Import rebase artifact cleanup functions
|
|
36
|
+
import { detectRebasedArtifacts, cleanupRebasedArtifacts } from './rebase-artifact-cleanup.js';
|
|
37
|
+
import { WUTransaction, createTransactionSnapshot, restoreFromSnapshot } from './wu-transaction.js';
|
|
38
|
+
// WU-1506: Import backlog invariant repair
|
|
39
|
+
// WU-1574: Removed repairBacklogInvariants - no longer needed with state store architecture
|
|
40
|
+
// Backlog.md is now always regenerated from wu-events.jsonl, so duplicates cannot occur
|
|
41
|
+
// WU-1584: Import retry helpers for squashing duplicate commits
|
|
42
|
+
// WU-1749: Added prepareRecoveryWithSquash for zombie recovery flow
|
|
43
|
+
import { countPreviousCompletionAttempts, squashPreviousCompletionAttempts, prepareRecoveryWithSquash, } from './wu-done-retry-helpers.js';
|
|
44
|
+
// WU-1747: Import retry, lock, and checkpoint modules for concurrent load resilience
|
|
45
|
+
import { withRetry, createRetryConfig } from './retry-strategy.js';
|
|
46
|
+
import { withMergeLock } from './merge-lock.js';
|
|
47
|
+
// WU-1749: Import state store constant for append-only file path
|
|
48
|
+
import { WU_EVENTS_FILE_NAME } from './wu-state-store.js';
|
|
49
|
+
import { validateWUEvent } from './wu-state-schema.js';
|
|
50
|
+
/**
|
|
51
|
+
* @typedef {Object} WorktreeContext
|
|
52
|
+
* @property {string} id - WU ID (e.g., "WU-1215")
|
|
53
|
+
* @property {Object} args - Parsed CLI arguments
|
|
54
|
+
* @property {Object} docMain - WU YAML document from main checkout
|
|
55
|
+
* @property {string} title - WU title for commit message
|
|
56
|
+
* @property {boolean} isDocsOnly - Whether this is a docs-only WU
|
|
57
|
+
* @property {string} worktreePath - Path to worktree
|
|
58
|
+
* @property {number} maxCommitLength - Max commit header length from commitlint
|
|
59
|
+
* @property {function} validateStagedFiles - Staged files validator
|
|
60
|
+
* NOTE: recordTransactionState/rollbackTransaction removed in WU-1369 (atomic pattern)
|
|
61
|
+
*/
|
|
62
|
+
/**
|
|
63
|
+
* @typedef {Object} WorktreeResult
|
|
64
|
+
* @property {boolean} success - Whether completion succeeded
|
|
65
|
+
* @property {boolean} committed - Whether changes were committed
|
|
66
|
+
* @property {boolean} pushed - Whether changes were pushed
|
|
67
|
+
* @property {boolean} merged - Whether lane branch was merged (vs PR created)
|
|
68
|
+
* @property {string|null} prUrl - PR URL if PR mode was used
|
|
69
|
+
* @property {boolean} [recovered] - Whether zombie state was recovered
|
|
70
|
+
* @property {boolean} [cleanupSafe] - WU-1811: Whether worktree cleanup is safe (all steps succeeded)
|
|
71
|
+
*/
|
|
72
|
+
/**
|
|
73
|
+
* Execute worktree mode completion
|
|
74
|
+
*
|
|
75
|
+
* @param {WorktreeContext} context - Worktree mode context
|
|
76
|
+
* @returns {Promise<WorktreeResult>} Completion result
|
|
77
|
+
* @throws {Error} On validation or git operation failure
|
|
78
|
+
*/
|
|
79
|
+
export async function executeWorktreeCompletion(context) {
|
|
80
|
+
// Save original cwd for returning after any worktree operations.
|
|
81
|
+
// This must be captured BEFORE zombie recovery, which temporarily chdirs into the worktree.
|
|
82
|
+
const originalCwd = process.cwd();
|
|
83
|
+
const { id, args, docMain, title, isDocsOnly, worktreePath, maxCommitLength, validateStagedFiles,
|
|
84
|
+
// NOTE: recordTransactionState/rollbackTransaction removed in WU-1369 (atomic pattern)
|
|
85
|
+
} = context;
|
|
86
|
+
// Calculate WU path relative to repo root (before chdir)
|
|
87
|
+
// Other paths are recalculated inside the worktree using WU_PATHS
|
|
88
|
+
const metadataWUPath = path.join(worktreePath, 'docs', '04-operations', 'tasks', 'wu', `${id}.yaml`);
|
|
89
|
+
// Read WU YAML and validate current state
|
|
90
|
+
const docForUpdate = readWU(metadataWUPath, id);
|
|
91
|
+
// Check for zombie state (recovery mode)
|
|
92
|
+
// WU-1440: If zombie state detected, reset worktree YAML to in_progress
|
|
93
|
+
// and continue with normal flow (don't commit directly to main)
|
|
94
|
+
if (detectZombieState(docForUpdate, worktreePath)) {
|
|
95
|
+
console.log(`\n${RECOVERY.DETECTED}`);
|
|
96
|
+
// WU-1335: Check recovery attempt count to prevent infinite loops
|
|
97
|
+
const attemptCount = getRecoveryAttemptCount(id);
|
|
98
|
+
if (shouldEscalateToManualIntervention(attemptCount)) {
|
|
99
|
+
console.log(`\n${BOX.TOP}`);
|
|
100
|
+
console.log(`${BOX.SIDE} RECOVERY LOOP DETECTED - MANUAL INTERVENTION REQUIRED`);
|
|
101
|
+
console.log(BOX.MID);
|
|
102
|
+
console.log(`${BOX.SIDE} WU: ${id}`);
|
|
103
|
+
console.log(`${BOX.SIDE} Recovery attempts: ${attemptCount} (max: ${MAX_RECOVERY_ATTEMPTS})`);
|
|
104
|
+
console.log(BOX.SIDE);
|
|
105
|
+
console.log(`${BOX.SIDE} Automatic recovery has failed multiple times.`);
|
|
106
|
+
console.log(`${BOX.SIDE} Manual steps required:`);
|
|
107
|
+
console.log(BOX.SIDE);
|
|
108
|
+
console.log(`${BOX.SIDE} 1. cd ${worktreePath}`);
|
|
109
|
+
console.log(`${BOX.SIDE} 2. Reset WU YAML status to in_progress manually`);
|
|
110
|
+
console.log(`${BOX.SIDE} 3. git add && git commit`);
|
|
111
|
+
console.log(`${BOX.SIDE} 4. Return to main and retry wu:done`);
|
|
112
|
+
console.log(BOX.SIDE);
|
|
113
|
+
console.log(`${BOX.SIDE} Or reset the recovery counter:`);
|
|
114
|
+
console.log(`${BOX.SIDE} rm .beacon/recovery/${id}.recovery`);
|
|
115
|
+
console.log(BOX.BOT);
|
|
116
|
+
throw createError(ErrorCodes.RECOVERY_ERROR, `Recovery loop detected for ${id} after ${attemptCount} attempts. Manual intervention required.`, { wuId: id, attemptCount, maxAttempts: MAX_RECOVERY_ATTEMPTS });
|
|
117
|
+
}
|
|
118
|
+
// Increment attempt counter before trying recovery
|
|
119
|
+
const newAttemptCount = incrementRecoveryAttempt(id);
|
|
120
|
+
console.log(`${LOG_PREFIX.DONE} Recovery attempt ${newAttemptCount}/${MAX_RECOVERY_ATTEMPTS} (WU-1335)`);
|
|
121
|
+
console.log(RECOVERY.RESUMING);
|
|
122
|
+
// WU-1749: Squash previous completion attempts before recovery
|
|
123
|
+
// This prevents "rebase hell" where multiple completion commits accumulate
|
|
124
|
+
// during failed retry attempts
|
|
125
|
+
try {
|
|
126
|
+
const prevCwd = process.cwd();
|
|
127
|
+
try {
|
|
128
|
+
process.chdir(worktreePath);
|
|
129
|
+
const gitCwd = getGitForCwd();
|
|
130
|
+
const squashResult = await prepareRecoveryWithSquash(id, gitCwd);
|
|
131
|
+
if (squashResult.squashedCount > 0) {
|
|
132
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Squashed ${squashResult.squashedCount} previous completion attempt(s)`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
process.chdir(prevCwd);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch (squashError) {
|
|
140
|
+
// Non-fatal: Log and continue with recovery
|
|
141
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Could not squash previous attempts: ${squashError.message}`);
|
|
142
|
+
}
|
|
143
|
+
console.log(`${LOG_PREFIX.DONE} WU-1440: Resetting worktree YAML to in_progress for recovery flow...`);
|
|
144
|
+
// Reset the worktree YAML to in_progress (mutates docForUpdate)
|
|
145
|
+
resetWorktreeYAMLForRecovery({ worktreePath, id, doc: docForUpdate });
|
|
146
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Recovery reset complete - continuing normal flow`);
|
|
147
|
+
// Continue with normal flow - don't return early
|
|
148
|
+
// docForUpdate is now status=in_progress, so normal flow will work
|
|
149
|
+
}
|
|
150
|
+
// Capture status AFTER potential zombie recovery reset
|
|
151
|
+
const currentStatus = docForUpdate.status || WU_STATUS.IN_PROGRESS;
|
|
152
|
+
// Validate state transition
|
|
153
|
+
assertTransition(currentStatus, WU_STATUS.DONE, id);
|
|
154
|
+
// WU-1369: Create atomic transaction for metadata updates
|
|
155
|
+
// This ensures NO files are modified if any validation fails
|
|
156
|
+
const transaction = new WUTransaction(id);
|
|
157
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Transaction BEGIN - atomic pattern (WU-1369)`);
|
|
158
|
+
// Save original cwd for returning after worktree operations
|
|
159
|
+
let merged = false;
|
|
160
|
+
let prUrl = null;
|
|
161
|
+
// WU-1943: Track pre-commit SHA and git commit state for rollback on merge failure
|
|
162
|
+
let preCommitSha = null;
|
|
163
|
+
let gitCommitMade = false;
|
|
164
|
+
// WU-2310: Track snapshot for file rollback on git commit failure
|
|
165
|
+
/** @type {Map<string, string|null>|null} */
|
|
166
|
+
let transactionSnapshot = null;
|
|
167
|
+
try {
|
|
168
|
+
// cd into worktree for metadata updates
|
|
169
|
+
console.log(`\n${LOG_PREFIX.DONE} Updating metadata in worktree: ${worktreePath}`);
|
|
170
|
+
process.chdir(worktreePath);
|
|
171
|
+
// Recalculate paths relative to worktree (now current dir)
|
|
172
|
+
const workingWUPath = WU_PATHS.WU(id);
|
|
173
|
+
const workingStatusPath = WU_PATHS.STATUS();
|
|
174
|
+
const workingBacklogPath = WU_PATHS.BACKLOG();
|
|
175
|
+
const workingStampsDir = WU_PATHS.STAMPS_DIR();
|
|
176
|
+
const workingStampPath = path.join(workingStampsDir, `${id}.done`);
|
|
177
|
+
// ======================================================================
|
|
178
|
+
// PHASE 1: RUN ALL VALIDATIONS FIRST (before any file writes)
|
|
179
|
+
// WU-1369: This ensures no partial state on validation failure
|
|
180
|
+
// WU-1811: Validate and normalize YAML before gates/merge
|
|
181
|
+
// ======================================================================
|
|
182
|
+
console.log(`${LOG_PREFIX.DONE} Running validations (no writes until all pass)...`);
|
|
183
|
+
// WU-1811: Validate and normalize WU YAML schema with fixable corrections
|
|
184
|
+
// This catches schema issues early and auto-fixes normalizable problems
|
|
185
|
+
const normalizeResult = validateAndNormalizeWUYAML(docForUpdate);
|
|
186
|
+
if (!normalizeResult.valid) {
|
|
187
|
+
throw createError(ErrorCodes.VALIDATION_ERROR, `WU YAML validation failed:\n - ${normalizeResult.errors.join('\n - ')}\n\nNext step: Fix the validation errors in ${workingWUPath} and rerun wu:done`, { wuId: id });
|
|
188
|
+
}
|
|
189
|
+
// WU-1811: If normalizations were applied, write back to YAML file
|
|
190
|
+
if (normalizeResult.wasNormalized) {
|
|
191
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.INFO} WU-1811: Applying auto-normalisations to WU YAML...`);
|
|
192
|
+
writeWU(workingWUPath, normalizeResult.normalized);
|
|
193
|
+
// Update docForUpdate to use normalized data for subsequent processing
|
|
194
|
+
Object.assign(docForUpdate, normalizeResult.normalized);
|
|
195
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} WU YAML normalised and saved`);
|
|
196
|
+
}
|
|
197
|
+
// Validate done-specific completeness (uses normalized data)
|
|
198
|
+
const completenessResult = validateDoneWU(normalizeResult.normalized);
|
|
199
|
+
if (!completenessResult.valid) {
|
|
200
|
+
throw createError(ErrorCodes.VALIDATION_ERROR, `Cannot mark WU as done - spec incomplete:\n ${completenessResult.errors.join('\n ')}\n\nNext step: Update ${workingWUPath} to meet completion requirements and rerun wu:done`, { wuId: id });
|
|
201
|
+
}
|
|
202
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} All validations passed`);
|
|
203
|
+
// ======================================================================
|
|
204
|
+
// PHASE 2: COLLECT ALL CHANGES TO TRANSACTION (in memory, no writes)
|
|
205
|
+
// WU-1369: Atomic collection - all changes gathered before any writes
|
|
206
|
+
// ======================================================================
|
|
207
|
+
// WU-1574: Now async
|
|
208
|
+
await collectMetadataToTransaction({
|
|
209
|
+
id,
|
|
210
|
+
title,
|
|
211
|
+
doc: docForUpdate,
|
|
212
|
+
wuPath: workingWUPath,
|
|
213
|
+
statusPath: workingStatusPath,
|
|
214
|
+
backlogPath: workingBacklogPath,
|
|
215
|
+
stampPath: workingStampPath,
|
|
216
|
+
transaction,
|
|
217
|
+
});
|
|
218
|
+
// Validate the transaction itself
|
|
219
|
+
const txValidation = transaction.validate();
|
|
220
|
+
if (!txValidation.valid) {
|
|
221
|
+
throw createError(ErrorCodes.TRANSACTION_ERROR, `Transaction validation failed:\n ${txValidation.errors.join('\n ')}`, { wuId: id });
|
|
222
|
+
}
|
|
223
|
+
// ======================================================================
|
|
224
|
+
// PHASE 3: ATOMIC COMMIT (write all files at once)
|
|
225
|
+
// WU-1369: This is the only point where files are written
|
|
226
|
+
// WU-2310: Capture snapshot BEFORE commit for rollback on git commit failure
|
|
227
|
+
// ======================================================================
|
|
228
|
+
// WU-2310: Capture file state before transaction commit
|
|
229
|
+
// This allows rollback if git commit fails AFTER files are written
|
|
230
|
+
// Note: We use the relative paths since we're already chdir'd into the worktree
|
|
231
|
+
const workingEventsPath = path.join('.beacon', 'state', WU_EVENTS_FILE_NAME);
|
|
232
|
+
const pathsToSnapshot = [
|
|
233
|
+
workingWUPath,
|
|
234
|
+
workingStatusPath,
|
|
235
|
+
workingBacklogPath,
|
|
236
|
+
workingStampPath,
|
|
237
|
+
workingEventsPath,
|
|
238
|
+
];
|
|
239
|
+
transactionSnapshot = createTransactionSnapshot(pathsToSnapshot);
|
|
240
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.INFO} WU-2310: Snapshot captured for rollback`);
|
|
241
|
+
const commitResult = transaction.commit();
|
|
242
|
+
if (!commitResult.success) {
|
|
243
|
+
throw createError(ErrorCodes.TRANSACTION_ERROR, `Transaction commit failed - some files not written:\n ${commitResult.failed.map((f) => f.path).join('\n ')}`, { wuId: id, written: commitResult.written, failed: commitResult.failed });
|
|
244
|
+
}
|
|
245
|
+
// ======================================================================
|
|
246
|
+
// WU-1617: POST-MUTATION VALIDATION
|
|
247
|
+
// Verify files written by tx.commit() are valid (completed_at, locked, stamp)
|
|
248
|
+
// ======================================================================
|
|
249
|
+
const postMutationResult = validatePostMutation({
|
|
250
|
+
id,
|
|
251
|
+
wuPath: workingWUPath,
|
|
252
|
+
stampPath: workingStampPath,
|
|
253
|
+
});
|
|
254
|
+
if (!postMutationResult.valid) {
|
|
255
|
+
throw createError(ErrorCodes.VALIDATION_ERROR, `Post-mutation validation failed:\n ${postMutationResult.errors.join('\n ')}`, { wuId: id, errors: postMutationResult.errors });
|
|
256
|
+
}
|
|
257
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Post-mutation validation passed (WU-1617)`);
|
|
258
|
+
// ======================================================================
|
|
259
|
+
// PHASE 4: GIT OPERATIONS (stage, format, commit)
|
|
260
|
+
// Files are now written - proceed with git operations
|
|
261
|
+
// ======================================================================
|
|
262
|
+
// Stage and format files
|
|
263
|
+
await stageAndFormatMetadata({
|
|
264
|
+
id,
|
|
265
|
+
wuPath: workingWUPath,
|
|
266
|
+
statusPath: workingStatusPath,
|
|
267
|
+
backlogPath: workingBacklogPath,
|
|
268
|
+
stampsDir: workingStampsDir,
|
|
269
|
+
});
|
|
270
|
+
// Validate staged files
|
|
271
|
+
await validateStagedFiles(id, isDocsOnly);
|
|
272
|
+
// ======================================================================
|
|
273
|
+
// WU-1584 Fix #1: Squash previous completion attempts before new commit
|
|
274
|
+
// This prevents N duplicate commits when wu:done is retried N times
|
|
275
|
+
// ======================================================================
|
|
276
|
+
const gitCwd = getGitForCwd();
|
|
277
|
+
const previousAttempts = await countPreviousCompletionAttempts(id, gitCwd);
|
|
278
|
+
if (previousAttempts > 0) {
|
|
279
|
+
const squashResult = await squashPreviousCompletionAttempts(id, previousAttempts, gitCwd);
|
|
280
|
+
if (squashResult.squashed) {
|
|
281
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} WU-1584: Squashed ${squashResult.count} previous attempt(s) - single commit will be created`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
// Generate commit message and commit
|
|
285
|
+
const msg = generateCommitMessage(id, title, maxCommitLength);
|
|
286
|
+
// WU-1943: Capture pre-commit SHA for rollback on merge failure
|
|
287
|
+
preCommitSha = await gitCwd.getCommitHash('HEAD');
|
|
288
|
+
await gitCwd.commit(msg);
|
|
289
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Metadata committed in worktree`);
|
|
290
|
+
// WU-1943: Track that git commit was made (needed for rollback decision)
|
|
291
|
+
gitCommitMade = true;
|
|
292
|
+
// Return to main checkout
|
|
293
|
+
process.chdir(originalCwd);
|
|
294
|
+
// Determine if PR mode is enabled
|
|
295
|
+
const prModeEnabled = isPRModeEnabled(docMain, args);
|
|
296
|
+
if (!args.noMerge) {
|
|
297
|
+
// Use docForUpdate (from worktree) for branch calculation - docMain may be incomplete (ref: WU-1280)
|
|
298
|
+
const laneBranch = await defaultBranchFrom(docForUpdate);
|
|
299
|
+
if (laneBranch && (await branchExists(laneBranch))) {
|
|
300
|
+
if (prModeEnabled) {
|
|
301
|
+
// PR mode: Create PR instead of auto-merge
|
|
302
|
+
const prResult = await createPR({
|
|
303
|
+
branch: laneBranch,
|
|
304
|
+
id,
|
|
305
|
+
title,
|
|
306
|
+
doc: docMain,
|
|
307
|
+
draft: args.prDraft,
|
|
308
|
+
});
|
|
309
|
+
if (prResult.success && prResult.prUrl) {
|
|
310
|
+
printPRCreatedMessage(prResult.prUrl, id);
|
|
311
|
+
prUrl = prResult.prUrl;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
// Default mode: Auto-merge with pre-flight checks
|
|
316
|
+
console.log(PREFLIGHT.RUNNING);
|
|
317
|
+
// Check branch drift (WU-1370: graduated warnings)
|
|
318
|
+
const commitsBehind = await checkBranchDrift(laneBranch);
|
|
319
|
+
if (commitsBehind > 0) {
|
|
320
|
+
const driftLevel = getDriftLevel(commitsBehind);
|
|
321
|
+
if (driftLevel === DRIFT_LEVELS.WARNING) {
|
|
322
|
+
console.log(PREFLIGHT.BRANCH_DRIFT_WARNING(commitsBehind));
|
|
323
|
+
}
|
|
324
|
+
else if (driftLevel === DRIFT_LEVELS.INFO) {
|
|
325
|
+
console.log(PREFLIGHT.BRANCH_DRIFT_INFO(commitsBehind));
|
|
326
|
+
}
|
|
327
|
+
else if (driftLevel === DRIFT_LEVELS.OK) {
|
|
328
|
+
// No message needed for OK level
|
|
329
|
+
console.log(PREFLIGHT.BRANCH_BEHIND(commitsBehind, THRESHOLDS.BRANCH_DRIFT_MAX));
|
|
330
|
+
}
|
|
331
|
+
// ERROR level is handled by checkBranchDrift throwing an error
|
|
332
|
+
}
|
|
333
|
+
// Check if branch is already merged
|
|
334
|
+
const alreadyMerged = await isBranchAlreadyMerged(laneBranch);
|
|
335
|
+
if (alreadyMerged) {
|
|
336
|
+
console.log(PREFLIGHT.ALREADY_MERGED);
|
|
337
|
+
console.log(PREFLIGHT.ALREADY_MERGED_EXPLANATION);
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
// Check for divergence and conflicts (auto-rebase if enabled - WU-1303)
|
|
341
|
+
// noAutoRebase is true when --no-auto-rebase flag is passed
|
|
342
|
+
// WU-1371: Pass wuId for post-rebase artifact cleanup
|
|
343
|
+
await checkBranchDivergence(laneBranch, {
|
|
344
|
+
autoRebase: args.noAutoRebase !== true,
|
|
345
|
+
worktreePath,
|
|
346
|
+
wuId: id,
|
|
347
|
+
});
|
|
348
|
+
// WU-1384: Check for merge commits (GitHub requires linear history)
|
|
349
|
+
// Must run AFTER divergence check, as divergence rebase may eliminate merge commits
|
|
350
|
+
// Catches case where user did 'git merge main' instead of rebase
|
|
351
|
+
// WU-1371: Pass wuId for post-rebase artifact cleanup
|
|
352
|
+
await checkMergeCommits(laneBranch, {
|
|
353
|
+
autoRebase: args.noAutoRebase !== true,
|
|
354
|
+
worktreePath,
|
|
355
|
+
wuId: id,
|
|
356
|
+
});
|
|
357
|
+
await checkMergeConflicts(laneBranch);
|
|
358
|
+
// WU-1456: Check for empty merge (warn if no work commits)
|
|
359
|
+
// WU-1460: Pass doc to enable code_paths blocker
|
|
360
|
+
await checkEmptyMerge(laneBranch, docForUpdate);
|
|
361
|
+
// WU-1574: Backlog repair removed - state store architecture eliminates duplicates
|
|
362
|
+
// Backlog.md is always regenerated from wu-events.jsonl, not parsed/modified
|
|
363
|
+
console.log(MERGE.STARTING(laneBranch));
|
|
364
|
+
// WU-1747: Wrap merge with lock for atomic operation under concurrent load
|
|
365
|
+
// WU-1749 Bug 2: Pass worktreePath and wuId for auto-rebase on retry
|
|
366
|
+
await withMergeLock(id, async () => {
|
|
367
|
+
await mergeLaneBranch(laneBranch, { worktreePath, wuId: id });
|
|
368
|
+
});
|
|
369
|
+
console.log(MERGE.ATOMIC_SUCCESS);
|
|
370
|
+
merged = true;
|
|
371
|
+
}
|
|
372
|
+
// Push from main
|
|
373
|
+
await getGitForCwd().push(REMOTES.ORIGIN, BRANCHES.MAIN);
|
|
374
|
+
console.log(MERGE.PUSHED(REMOTES.ORIGIN, BRANCHES.MAIN));
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
else {
|
|
378
|
+
// Branch not found - fail loudly (use docForUpdate which has complete lane info)
|
|
379
|
+
console.error(`\n${BOX.TOP}`);
|
|
380
|
+
console.error(`${BOX.SIDE} MERGE FAILED: Lane branch not found`);
|
|
381
|
+
console.error(BOX.MID);
|
|
382
|
+
console.error(`${BOX.SIDE} Expected branch: ${laneBranch || '(null)'}`);
|
|
383
|
+
console.error(`${BOX.SIDE} WU lane: "${docForUpdate.lane}"`);
|
|
384
|
+
console.error(`${BOX.SIDE} WU id: "${docForUpdate.id}"`);
|
|
385
|
+
console.error(BOX.BOT);
|
|
386
|
+
throw createError(ErrorCodes.BRANCH_ERROR, `Lane branch not found: ${laneBranch}`, {
|
|
387
|
+
laneBranch,
|
|
388
|
+
wuId: docForUpdate.id,
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
// WU-1335: Clear recovery attempts on successful completion
|
|
393
|
+
clearRecoveryAttempts(id);
|
|
394
|
+
// WU-1811: All steps succeeded - worktree cleanup is safe
|
|
395
|
+
return {
|
|
396
|
+
success: true,
|
|
397
|
+
committed: true,
|
|
398
|
+
pushed: !prModeEnabled,
|
|
399
|
+
merged,
|
|
400
|
+
prUrl,
|
|
401
|
+
cleanupSafe: true,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
catch (err) {
|
|
405
|
+
// Restore original directory
|
|
406
|
+
try {
|
|
407
|
+
process.chdir(originalCwd);
|
|
408
|
+
}
|
|
409
|
+
catch {
|
|
410
|
+
// Ignore chdir errors during cleanup
|
|
411
|
+
}
|
|
412
|
+
// WU-1369: Atomic transaction pattern
|
|
413
|
+
// - If error occurred BEFORE transaction.commit() → no files were written
|
|
414
|
+
// - If error occurred AFTER transaction.commit() → files written, need manual recovery
|
|
415
|
+
const wasCommitted = transaction.isCommitted;
|
|
416
|
+
// WU-1811: Provide actionable single next step based on failure state
|
|
417
|
+
if (!wasCommitted) {
|
|
418
|
+
// Abort transaction (discards pending changes, no files were written)
|
|
419
|
+
transaction.abort();
|
|
420
|
+
console.log(`\n${BOX.TOP}`);
|
|
421
|
+
console.log(`${BOX.SIDE} WU:DONE FAILED - NO FILES MODIFIED (atomic pattern)`);
|
|
422
|
+
console.log(BOX.MID);
|
|
423
|
+
console.log(`${BOX.SIDE} Error: ${err.message}`);
|
|
424
|
+
console.log(BOX.SIDE);
|
|
425
|
+
console.log(`${BOX.SIDE} WU-1369: Transaction aborted before any writes.`);
|
|
426
|
+
console.log(`${BOX.SIDE} WU-1811: Worktree preserved for recovery.`);
|
|
427
|
+
console.log(`${BOX.SIDE} Worktree: ${worktreePath}`);
|
|
428
|
+
console.log(BOX.MID);
|
|
429
|
+
console.log(`${BOX.SIDE} NEXT STEP: Fix the error and rerun:`);
|
|
430
|
+
console.log(`${BOX.SIDE} pnpm wu:done --id ${id}`);
|
|
431
|
+
console.log(BOX.BOT);
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
// Transaction was committed but git operations failed
|
|
435
|
+
// Files were written - need rollback or recovery
|
|
436
|
+
// WU-2310: Rollback file changes if git commit failed (before branch commit was made)
|
|
437
|
+
// This prevents zombie states where status=done but commit never happened
|
|
438
|
+
let fileRollbackSuccess = false;
|
|
439
|
+
if (!gitCommitMade && transactionSnapshot) {
|
|
440
|
+
console.log(`\n${LOG_PREFIX.DONE} ${EMOJI.WARNING} WU-2310: Git commit failed after transaction - rolling back files...`);
|
|
441
|
+
try {
|
|
442
|
+
// cd into worktree for rollback
|
|
443
|
+
process.chdir(worktreePath);
|
|
444
|
+
const rollbackResult = restoreFromSnapshot(transactionSnapshot);
|
|
445
|
+
if (rollbackResult.errors.length === 0) {
|
|
446
|
+
fileRollbackSuccess = true;
|
|
447
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} WU-2310: File rollback complete - ${rollbackResult.restored.length} files restored`);
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} WU-2310: Partial file rollback - ${rollbackResult.restored.length} restored, ${rollbackResult.errors.length} failed`);
|
|
451
|
+
for (const e of rollbackResult.errors) {
|
|
452
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.FAILURE} ${e.path}: ${e.error}`);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
// Return to main checkout
|
|
456
|
+
process.chdir(originalCwd);
|
|
457
|
+
}
|
|
458
|
+
catch (rollbackErr) {
|
|
459
|
+
// Log but don't fail - rollback is best-effort
|
|
460
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} WU-2310: File rollback error: ${rollbackErr.message}`);
|
|
461
|
+
try {
|
|
462
|
+
process.chdir(originalCwd);
|
|
463
|
+
}
|
|
464
|
+
catch {
|
|
465
|
+
// Ignore chdir errors
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
// WU-1943: If git commit was made but merge failed, rollback the branch
|
|
470
|
+
// This prevents zombie states where branch shows "done" but wasn't merged
|
|
471
|
+
if (gitCommitMade && preCommitSha) {
|
|
472
|
+
console.log(`\n${LOG_PREFIX.DONE} ${EMOJI.WARNING} Merge failed after git commit - attempting branch rollback...`);
|
|
473
|
+
try {
|
|
474
|
+
// cd into worktree for rollback
|
|
475
|
+
process.chdir(worktreePath);
|
|
476
|
+
const gitCwd = getGitForCwd();
|
|
477
|
+
await rollbackBranchOnMergeFailure(gitCwd, preCommitSha, id);
|
|
478
|
+
// Return to main checkout
|
|
479
|
+
process.chdir(originalCwd);
|
|
480
|
+
}
|
|
481
|
+
catch (rollbackErr) {
|
|
482
|
+
// Log but don't fail - rollback is best-effort
|
|
483
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Rollback error: ${rollbackErr.message}`);
|
|
484
|
+
try {
|
|
485
|
+
process.chdir(originalCwd);
|
|
486
|
+
}
|
|
487
|
+
catch {
|
|
488
|
+
// Ignore chdir errors
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
console.log(`\n${BOX.TOP}`);
|
|
493
|
+
if (fileRollbackSuccess) {
|
|
494
|
+
console.log(`${BOX.SIDE} WU:DONE FAILED - FILES ROLLED BACK (WU-2310)`);
|
|
495
|
+
console.log(BOX.MID);
|
|
496
|
+
console.log(`${BOX.SIDE} Error: ${err.message}`);
|
|
497
|
+
console.log(BOX.SIDE);
|
|
498
|
+
console.log(`${BOX.SIDE} WU-2310: Transaction files were rolled back to pre-commit state.`);
|
|
499
|
+
console.log(`${BOX.SIDE} Worktree is now consistent (status=in_progress, no stamp).`);
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
console.log(`${BOX.SIDE} WU:DONE FAILED - PARTIAL STATE (post-transaction)`);
|
|
503
|
+
console.log(BOX.MID);
|
|
504
|
+
console.log(`${BOX.SIDE} Error: ${err.message}`);
|
|
505
|
+
console.log(BOX.SIDE);
|
|
506
|
+
console.log(`${BOX.SIDE} Metadata files were written, but git operations failed.`);
|
|
507
|
+
if (gitCommitMade && preCommitSha) {
|
|
508
|
+
console.log(`${BOX.SIDE} WU-1943: Branch rolled back to pre-commit state.`);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
console.log(`${BOX.SIDE} WU-1811: Worktree preserved for recovery.`);
|
|
512
|
+
console.log(`${BOX.SIDE} Worktree: ${worktreePath}`);
|
|
513
|
+
console.log(BOX.MID);
|
|
514
|
+
console.log(`${BOX.SIDE} NEXT STEP: Rerun wu:done (idempotent recovery):`);
|
|
515
|
+
console.log(`${BOX.SIDE} pnpm wu:done --id ${id}`);
|
|
516
|
+
console.log(BOX.BOT);
|
|
517
|
+
}
|
|
518
|
+
// WU-1811: Attach cleanupSafe flag to error for caller to check
|
|
519
|
+
err.cleanupSafe = false;
|
|
520
|
+
throw err;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Check for branch drift (commits behind main)
|
|
525
|
+
* WU-755 pre-flight check
|
|
526
|
+
*
|
|
527
|
+
* @param {string} branch - Lane branch name
|
|
528
|
+
* @returns {Promise<number>} Number of commits behind main
|
|
529
|
+
*/
|
|
530
|
+
export async function checkBranchDrift(branch) {
|
|
531
|
+
const gitAdapter = getGitForCwd();
|
|
532
|
+
try {
|
|
533
|
+
const counts = await gitAdapter.revList([
|
|
534
|
+
'--left-right',
|
|
535
|
+
'--count',
|
|
536
|
+
`${BRANCHES.MAIN}...${branch}`,
|
|
537
|
+
]);
|
|
538
|
+
const [mainAhead] = counts.split(/\s+/).map(Number);
|
|
539
|
+
if (mainAhead > THRESHOLDS.BRANCH_DRIFT_MAX) {
|
|
540
|
+
throw createError(ErrorCodes.GIT_ERROR, PREFLIGHT.BRANCH_DRIFT_ERROR(mainAhead, THRESHOLDS.BRANCH_DRIFT_MAX, REMOTES.ORIGIN, BRANCHES.MAIN), { branch, commitsBehind: mainAhead, threshold: THRESHOLDS.BRANCH_DRIFT_MAX });
|
|
541
|
+
}
|
|
542
|
+
return mainAhead;
|
|
543
|
+
}
|
|
544
|
+
catch (e) {
|
|
545
|
+
if (e.code === ErrorCodes.GIT_ERROR)
|
|
546
|
+
throw e;
|
|
547
|
+
console.warn(`${LOG_PREFIX.DONE} Warning: Could not check branch drift: ${e.message}`);
|
|
548
|
+
return 0;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* List of append-only files that can be auto-resolved during rebase
|
|
553
|
+
* WU-1749 Bug 3: These files can safely have conflicts resolved by keeping both additions
|
|
554
|
+
*
|
|
555
|
+
* Uses WU_PATHS constants and WU_EVENTS_FILE_NAME to avoid hardcoded path strings
|
|
556
|
+
* that would break if paths are rearranged.
|
|
557
|
+
*/
|
|
558
|
+
const APPEND_ONLY_FILES = [
|
|
559
|
+
// State store events file (append-only by design)
|
|
560
|
+
path.join('.beacon', 'state', WU_EVENTS_FILE_NAME),
|
|
561
|
+
// Status and backlog are generated from state store but may conflict during rebase
|
|
562
|
+
WU_PATHS.STATUS(),
|
|
563
|
+
WU_PATHS.BACKLOG(),
|
|
564
|
+
];
|
|
565
|
+
const WU_EVENTS_PATH = path.join('.beacon', 'state', WU_EVENTS_FILE_NAME);
|
|
566
|
+
function normalizeEventForKey(event) {
|
|
567
|
+
const normalized = {};
|
|
568
|
+
for (const key of Object.keys(event).sort()) {
|
|
569
|
+
// eslint-disable-next-line security/detect-object-injection -- keys derived from object keys
|
|
570
|
+
normalized[key] = event[key];
|
|
571
|
+
}
|
|
572
|
+
return normalized;
|
|
573
|
+
}
|
|
574
|
+
function parseWuEventsJsonl(content, sourceLabel) {
|
|
575
|
+
const lines = String(content)
|
|
576
|
+
.split('\n')
|
|
577
|
+
.map((l) => l.trim())
|
|
578
|
+
.filter(Boolean);
|
|
579
|
+
return lines.map((line, index) => {
|
|
580
|
+
let parsed;
|
|
581
|
+
try {
|
|
582
|
+
parsed = JSON.parse(line);
|
|
583
|
+
}
|
|
584
|
+
catch (error) {
|
|
585
|
+
throw new Error(`wu-events.jsonl ${sourceLabel} has malformed JSON on line ${index + 1}: ${error.message}`);
|
|
586
|
+
}
|
|
587
|
+
const validation = validateWUEvent(parsed);
|
|
588
|
+
if (!validation.success) {
|
|
589
|
+
const issues = validation.error.issues
|
|
590
|
+
.map((issue) => `${issue.path.join('.')}: ${issue.message}`)
|
|
591
|
+
.join(', ');
|
|
592
|
+
throw new Error(`wu-events.jsonl ${sourceLabel} has invalid event on line ${index + 1}: ${issues}`);
|
|
593
|
+
}
|
|
594
|
+
return { event: validation.data, line };
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
async function resolveWuEventsJsonlConflict(gitCwd, filePath) {
|
|
598
|
+
const ours = await gitCwd.raw(['show', `:2:${filePath}`]);
|
|
599
|
+
const theirs = await gitCwd.raw(['show', `:3:${filePath}`]);
|
|
600
|
+
const theirsEvents = parseWuEventsJsonl(theirs, 'theirs');
|
|
601
|
+
const oursEvents = parseWuEventsJsonl(ours, 'ours');
|
|
602
|
+
const seen = new Set();
|
|
603
|
+
const mergedLines = [];
|
|
604
|
+
for (const { event, line } of theirsEvents) {
|
|
605
|
+
const key = JSON.stringify(normalizeEventForKey(event));
|
|
606
|
+
if (seen.has(key))
|
|
607
|
+
continue;
|
|
608
|
+
seen.add(key);
|
|
609
|
+
mergedLines.push(line);
|
|
610
|
+
}
|
|
611
|
+
for (const { event, line } of oursEvents) {
|
|
612
|
+
const key = JSON.stringify(normalizeEventForKey(event));
|
|
613
|
+
if (seen.has(key))
|
|
614
|
+
continue;
|
|
615
|
+
seen.add(key);
|
|
616
|
+
mergedLines.push(line);
|
|
617
|
+
}
|
|
618
|
+
await writeFile(filePath, mergedLines.join('\n') + '\n', 'utf-8');
|
|
619
|
+
await gitCwd.add(filePath);
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* Auto-resolve conflicts in append-only files during rebase
|
|
623
|
+
* WU-1749 Bug 3: Keeps both additions for append-only files
|
|
624
|
+
*
|
|
625
|
+
* @param {object} gitCwd - Git adapter instance
|
|
626
|
+
* @returns {Promise<{resolved: boolean, files: string[]}>} Resolution result
|
|
627
|
+
*/
|
|
628
|
+
async function autoResolveAppendOnlyConflicts(gitCwd) {
|
|
629
|
+
const resolvedFiles = [];
|
|
630
|
+
try {
|
|
631
|
+
// Get list of conflicted files
|
|
632
|
+
const status = await gitCwd.getStatus();
|
|
633
|
+
const conflictLines = status.split('\n').filter((line) => line.startsWith('UU '));
|
|
634
|
+
for (const line of conflictLines) {
|
|
635
|
+
const filePath = line.substring(3).trim();
|
|
636
|
+
// Check if this is an append-only file
|
|
637
|
+
const isAppendOnly = APPEND_ONLY_FILES.some((appendFile) => filePath.endsWith(appendFile) || filePath.includes(appendFile));
|
|
638
|
+
if (isAppendOnly) {
|
|
639
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.INFO} Auto-resolving append-only conflict: ${filePath}`);
|
|
640
|
+
if (filePath.endsWith(WU_EVENTS_PATH) || filePath.includes(WU_EVENTS_PATH)) {
|
|
641
|
+
// For the event log we must keep BOTH sides (loss breaks state machine).
|
|
642
|
+
// Merge strategy: union by event identity (validated), prefer theirs ordering then ours additions.
|
|
643
|
+
await resolveWuEventsJsonlConflict(gitCwd, filePath);
|
|
644
|
+
}
|
|
645
|
+
else {
|
|
646
|
+
// Backlog/status are derived; prefer main's version during rebase and regenerate later.
|
|
647
|
+
await gitCwd.raw(['checkout', '--theirs', filePath]);
|
|
648
|
+
await gitCwd.add(filePath);
|
|
649
|
+
}
|
|
650
|
+
resolvedFiles.push(filePath);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
return { resolved: resolvedFiles.length > 0, files: resolvedFiles };
|
|
654
|
+
}
|
|
655
|
+
catch (error) {
|
|
656
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Could not auto-resolve conflicts: ${error.message}`);
|
|
657
|
+
return { resolved: false, files: [] };
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Auto-rebase branch onto main
|
|
662
|
+
* WU-1303: Auto-rebase on wu:done to handle diverged branches automatically
|
|
663
|
+
* WU-1371: Added wuId parameter for post-rebase artifact cleanup
|
|
664
|
+
* WU-1749 Bug 3: Auto-resolve append-only file conflicts during rebase
|
|
665
|
+
*
|
|
666
|
+
* @param {string} branch - Lane branch name
|
|
667
|
+
* @param {string} worktreePath - Path to worktree
|
|
668
|
+
* @param {string} [wuId] - WU ID for artifact cleanup (e.g., 'WU-1371')
|
|
669
|
+
* @returns {Promise<{success: boolean, error?: string}>} Rebase result
|
|
670
|
+
*/
|
|
671
|
+
export async function autoRebaseBranch(branch, worktreePath, wuId) {
|
|
672
|
+
console.log(REBASE.STARTING(branch, BRANCHES.MAIN));
|
|
673
|
+
// Save original cwd
|
|
674
|
+
const originalCwd = process.cwd();
|
|
675
|
+
const previousEditor = process.env.GIT_EDITOR;
|
|
676
|
+
process.env.GIT_EDITOR = 'true';
|
|
677
|
+
try {
|
|
678
|
+
// cd into worktree for rebase
|
|
679
|
+
process.chdir(worktreePath);
|
|
680
|
+
const gitCwd = getGitForCwd();
|
|
681
|
+
// Fetch latest main
|
|
682
|
+
await gitCwd.fetch(REMOTES.ORIGIN, BRANCHES.MAIN);
|
|
683
|
+
// Attempt rebase
|
|
684
|
+
try {
|
|
685
|
+
await gitCwd.rebase(`${REMOTES.ORIGIN}/${BRANCHES.MAIN}`);
|
|
686
|
+
}
|
|
687
|
+
catch (rebaseError) {
|
|
688
|
+
// WU-1749 Bug 3: Check if conflicts are in append-only files that can be auto-resolved
|
|
689
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.INFO} Rebase hit conflicts, checking for auto-resolvable...`);
|
|
690
|
+
const resolution = await autoResolveAppendOnlyConflicts(gitCwd);
|
|
691
|
+
if (resolution.resolved) {
|
|
692
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Auto-resolved ${resolution.files.length} append-only conflict(s)`);
|
|
693
|
+
// Continue the rebase after resolving conflicts
|
|
694
|
+
try {
|
|
695
|
+
await gitCwd.raw(['rebase', '--continue']);
|
|
696
|
+
}
|
|
697
|
+
catch (continueError) {
|
|
698
|
+
// May need multiple rounds of conflict resolution
|
|
699
|
+
// For simplicity, we'll try once more
|
|
700
|
+
const secondResolution = await autoResolveAppendOnlyConflicts(gitCwd);
|
|
701
|
+
if (secondResolution.resolved) {
|
|
702
|
+
await gitCwd.raw(['rebase', '--continue']);
|
|
703
|
+
}
|
|
704
|
+
else {
|
|
705
|
+
// Still have non-auto-resolvable conflicts
|
|
706
|
+
throw continueError;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
else {
|
|
711
|
+
// No auto-resolvable conflicts - rethrow original error
|
|
712
|
+
throw rebaseError;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
// WU-1371: Detect and cleanup rebased completion artifacts
|
|
716
|
+
// After rebase, check if main's completion artifacts (stamps, status=done)
|
|
717
|
+
// were pulled into the worktree. These must be cleaned before continuing.
|
|
718
|
+
// WU-1817: Now passes gitCwd to verify artifacts exist on origin/main
|
|
719
|
+
if (wuId) {
|
|
720
|
+
const artifacts = await detectRebasedArtifacts(worktreePath, wuId, gitCwd);
|
|
721
|
+
if (artifacts.hasArtifacts) {
|
|
722
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Detected rebased completion artifacts`);
|
|
723
|
+
const cleanup = await cleanupRebasedArtifacts(worktreePath, wuId);
|
|
724
|
+
if (cleanup.cleaned) {
|
|
725
|
+
// Stage and commit the cleanup
|
|
726
|
+
await gitCwd.add('.');
|
|
727
|
+
await gitCwd.commit(COMMIT_FORMATS.REBASE_ARTIFACT_CLEANUP(wuId));
|
|
728
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Cleaned rebased artifacts and committed`);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
// Force-push lane branch with lease (safe force push)
|
|
733
|
+
await gitCwd.raw(['push', '--force-with-lease', REMOTES.ORIGIN, branch]);
|
|
734
|
+
console.log(REBASE.SUCCESS);
|
|
735
|
+
return { success: true };
|
|
736
|
+
}
|
|
737
|
+
catch (e) {
|
|
738
|
+
// Rebase failed (likely conflicts) - abort and report
|
|
739
|
+
console.error(REBASE.FAILED(e.message));
|
|
740
|
+
try {
|
|
741
|
+
// Abort the failed rebase to leave worktree clean
|
|
742
|
+
const gitCwd = getGitForCwd();
|
|
743
|
+
await gitCwd.raw(['rebase', '--abort']);
|
|
744
|
+
console.log(REBASE.ABORTED);
|
|
745
|
+
}
|
|
746
|
+
catch {
|
|
747
|
+
// Ignore abort errors - may already be clean
|
|
748
|
+
}
|
|
749
|
+
return {
|
|
750
|
+
success: false,
|
|
751
|
+
error: REBASE.MANUAL_FIX(worktreePath, REMOTES.ORIGIN, BRANCHES.MAIN, branch),
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
finally {
|
|
755
|
+
if (previousEditor === undefined) {
|
|
756
|
+
delete process.env.GIT_EDITOR;
|
|
757
|
+
}
|
|
758
|
+
else {
|
|
759
|
+
process.env.GIT_EDITOR = previousEditor;
|
|
760
|
+
}
|
|
761
|
+
// Always return to original directory
|
|
762
|
+
try {
|
|
763
|
+
process.chdir(originalCwd);
|
|
764
|
+
}
|
|
765
|
+
catch {
|
|
766
|
+
// Ignore chdir errors
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
export async function checkBranchDivergence(branch, options = {}) {
|
|
771
|
+
const { autoRebase = true, worktreePath = null, wuId = null } = options;
|
|
772
|
+
const gitAdapter = getGitForCwd();
|
|
773
|
+
try {
|
|
774
|
+
const mergeBase = await gitAdapter.mergeBase(BRANCHES.MAIN, branch);
|
|
775
|
+
const mainHead = await gitAdapter.getCommitHash(BRANCHES.MAIN);
|
|
776
|
+
if (mergeBase !== mainHead) {
|
|
777
|
+
const mainCommitsAhead = await gitAdapter.revList([
|
|
778
|
+
'--count',
|
|
779
|
+
`${mergeBase}..${BRANCHES.MAIN}`,
|
|
780
|
+
]);
|
|
781
|
+
const commitCount = Number(mainCommitsAhead);
|
|
782
|
+
console.log(PREFLIGHT.DIVERGENCE_DETECTED(commitCount));
|
|
783
|
+
// Attempt auto-rebase if enabled and worktree path provided
|
|
784
|
+
if (autoRebase && worktreePath) {
|
|
785
|
+
const rebaseResult = await autoRebaseBranch(branch, worktreePath, wuId);
|
|
786
|
+
if (rebaseResult.success) {
|
|
787
|
+
// Rebase succeeded - continue with wu:done
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
// Rebase failed - throw with detailed instructions
|
|
791
|
+
throw createError(ErrorCodes.GIT_ERROR, rebaseResult.error, {
|
|
792
|
+
branch,
|
|
793
|
+
mergeBase,
|
|
794
|
+
mainHead,
|
|
795
|
+
mainCommitsAhead: commitCount,
|
|
796
|
+
autoRebaseAttempted: true,
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
// Auto-rebase disabled or no worktree path - throw with manual instructions
|
|
800
|
+
throw createError(ErrorCodes.GIT_ERROR, PREFLIGHT.DIVERGENCE_ERROR(commitCount, REMOTES.ORIGIN, BRANCHES.MAIN, branch), { branch, mergeBase, mainHead, mainCommitsAhead: commitCount });
|
|
801
|
+
}
|
|
802
|
+
console.log(PREFLIGHT.NO_DIVERGENCE);
|
|
803
|
+
}
|
|
804
|
+
catch (e) {
|
|
805
|
+
if (e.code === ErrorCodes.GIT_ERROR)
|
|
806
|
+
throw e;
|
|
807
|
+
console.warn(`${LOG_PREFIX.DONE} Warning: Could not check branch divergence: ${e.message}`);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* Check for merge commits in lane branch that would violate linear history
|
|
812
|
+
* WU-1384: GitHub requires linear history; merge commits in lane branches must be eliminated
|
|
813
|
+
* WU-1371: Added wuId option for post-rebase artifact cleanup
|
|
814
|
+
*
|
|
815
|
+
* If merge commits are found, triggers auto-rebase to linearize history.
|
|
816
|
+
*
|
|
817
|
+
* @param {string} branch - Lane branch name
|
|
818
|
+
* @param {CheckBranchOptions} [options] - Check options
|
|
819
|
+
* @throws {Error} If merge commits found and auto-rebase fails or is disabled
|
|
820
|
+
*/
|
|
821
|
+
export async function checkMergeCommits(branch, options = {}) {
|
|
822
|
+
const { autoRebase = true, worktreePath = null, wuId = null } = options;
|
|
823
|
+
const gitAdapter = getGitForCwd();
|
|
824
|
+
try {
|
|
825
|
+
// Find merge commits in lane branch that are not in main
|
|
826
|
+
// --merges: only merge commits
|
|
827
|
+
// main..branch: commits in branch not reachable from main
|
|
828
|
+
const mergeCommitsRaw = await gitAdapter.raw([
|
|
829
|
+
'rev-list',
|
|
830
|
+
'--merges',
|
|
831
|
+
`${BRANCHES.MAIN}..${branch}`,
|
|
832
|
+
]);
|
|
833
|
+
const mergeCommits = mergeCommitsRaw.trim().split(STRING_LITERALS.NEWLINE).filter(Boolean);
|
|
834
|
+
const mergeCount = mergeCommits.length;
|
|
835
|
+
if (mergeCount > 0) {
|
|
836
|
+
console.log(PREFLIGHT.MERGE_COMMITS_DETECTED(mergeCount));
|
|
837
|
+
// Trigger rebase to eliminate merge commits
|
|
838
|
+
if (autoRebase && worktreePath) {
|
|
839
|
+
console.log(PREFLIGHT.MERGE_COMMITS_REBASING);
|
|
840
|
+
const rebaseResult = await autoRebaseBranch(branch, worktreePath, wuId);
|
|
841
|
+
if (rebaseResult.success) {
|
|
842
|
+
// Rebase succeeded - merge commits eliminated
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
// Rebase failed - throw with detailed instructions
|
|
846
|
+
throw createError(ErrorCodes.GIT_ERROR, rebaseResult.error, {
|
|
847
|
+
branch,
|
|
848
|
+
mergeCommitCount: mergeCount,
|
|
849
|
+
autoRebaseAttempted: true,
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
// Auto-rebase disabled or no worktree path - throw with manual instructions
|
|
853
|
+
throw createError(ErrorCodes.GIT_ERROR, `Branch ${branch} contains ${mergeCount} merge commit(s).\n\n` +
|
|
854
|
+
`GitHub requires linear history. Merge commits must be eliminated.\n\n` +
|
|
855
|
+
`REQUIRED: Rebase your branch to linearize history:\n` +
|
|
856
|
+
` 1. cd into your worktree\n` +
|
|
857
|
+
` 2. git fetch ${REMOTES.ORIGIN} ${BRANCHES.MAIN}\n` +
|
|
858
|
+
` 3. git rebase ${REMOTES.ORIGIN}/${BRANCHES.MAIN}\n` +
|
|
859
|
+
` 4. git push --force-with-lease ${REMOTES.ORIGIN} ${branch}\n` +
|
|
860
|
+
` 5. Return to main checkout and retry`, { branch, mergeCommitCount: mergeCount });
|
|
861
|
+
}
|
|
862
|
+
console.log(PREFLIGHT.NO_MERGE_COMMITS);
|
|
863
|
+
}
|
|
864
|
+
catch (e) {
|
|
865
|
+
if (e.code === ErrorCodes.GIT_ERROR)
|
|
866
|
+
throw e;
|
|
867
|
+
console.warn(`${LOG_PREFIX.DONE} Warning: Could not check for merge commits: ${e.message}`);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* Check for merge conflicts using git merge-tree
|
|
872
|
+
* WU-755 pre-flight check
|
|
873
|
+
*
|
|
874
|
+
* @param {string} branch - Lane branch name
|
|
875
|
+
*/
|
|
876
|
+
export async function checkMergeConflicts(branch) {
|
|
877
|
+
const gitAdapter = getGitForCwd();
|
|
878
|
+
try {
|
|
879
|
+
const mergeBase = await gitAdapter.mergeBase(BRANCHES.MAIN, branch);
|
|
880
|
+
const result = await gitAdapter.mergeTree(mergeBase, BRANCHES.MAIN, branch);
|
|
881
|
+
if (result.includes('<<<<<<<') || result.includes('>>>>>>>')) {
|
|
882
|
+
throw createError(ErrorCodes.GIT_ERROR, PREFLIGHT.CONFLICT_ERROR, {
|
|
883
|
+
branch,
|
|
884
|
+
operation: 'merge-tree',
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
console.log(PREFLIGHT.NO_CONFLICTS);
|
|
888
|
+
}
|
|
889
|
+
catch (e) {
|
|
890
|
+
if (e.code === ErrorCodes.GIT_ERROR)
|
|
891
|
+
throw e;
|
|
892
|
+
console.warn(`${LOG_PREFIX.DONE} Warning: Could not check merge conflicts: ${e.message}`);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
/**
|
|
896
|
+
* WU-1456: Check for empty merge (no work commits beyond claim)
|
|
897
|
+
* WU-1460: Upgraded to BLOCK when code_paths defined but files not modified
|
|
898
|
+
*
|
|
899
|
+
* Detects when an agent runs wu:done without committing actual work.
|
|
900
|
+
* - If code_paths defined: BLOCK if those files weren't modified
|
|
901
|
+
* - If no code_paths: WARNING only (docs-only or metadata updates are valid)
|
|
902
|
+
*
|
|
903
|
+
* @param {string} branch - Lane branch name
|
|
904
|
+
* @param {object} [doc] - WU document with code_paths array (optional for backwards compatibility)
|
|
905
|
+
* @returns {Promise<void>}
|
|
906
|
+
* @throws {Error} When code_paths defined but files not modified in commits
|
|
907
|
+
*/
|
|
908
|
+
export async function checkEmptyMerge(branch, doc = null) {
|
|
909
|
+
const gitAdapter = getGitForCwd();
|
|
910
|
+
try {
|
|
911
|
+
// Count commits on lane branch that are not in main
|
|
912
|
+
const commitCountRaw = await gitAdapter.raw([
|
|
913
|
+
'rev-list',
|
|
914
|
+
'--count',
|
|
915
|
+
`${BRANCHES.MAIN}..${branch}`,
|
|
916
|
+
]);
|
|
917
|
+
const commitCount = Number(commitCountRaw.trim());
|
|
918
|
+
// WU-1460: If code_paths defined, verify those files were modified
|
|
919
|
+
const codePaths = doc?.code_paths || [];
|
|
920
|
+
const hasCodePaths = Array.isArray(codePaths) && codePaths.length > 0;
|
|
921
|
+
if (hasCodePaths) {
|
|
922
|
+
// Get list of files modified in lane branch commits
|
|
923
|
+
const modifiedFilesRaw = await gitAdapter.raw([
|
|
924
|
+
'diff',
|
|
925
|
+
'--name-only',
|
|
926
|
+
`${BRANCHES.MAIN}...${branch}`,
|
|
927
|
+
]);
|
|
928
|
+
const modifiedFiles = modifiedFilesRaw.trim().split('\n').filter(Boolean);
|
|
929
|
+
// Check if any code_paths files are in the modified list
|
|
930
|
+
const missingCodePaths = codePaths.filter((codePath) => !modifiedFiles.some((modified) => modified.includes(codePath) || codePath.includes(modified)));
|
|
931
|
+
if (missingCodePaths.length > 0) {
|
|
932
|
+
// BLOCK: code_paths defined but files not modified
|
|
933
|
+
throw createError(ErrorCodes.VALIDATION_ERROR, PREFLIGHT.CODE_PATHS_NOT_MODIFIED(missingCodePaths), { branch, codePaths, missingCodePaths, modifiedFiles });
|
|
934
|
+
}
|
|
935
|
+
// All code_paths files were modified
|
|
936
|
+
console.log(PREFLIGHT.CODE_PATHS_VERIFIED);
|
|
937
|
+
}
|
|
938
|
+
else if (commitCount <= 1) {
|
|
939
|
+
// No code_paths - just warn (backwards compatible behaviour)
|
|
940
|
+
// If only 0-1 commits beyond main, this is likely the claim commit only
|
|
941
|
+
console.log(PREFLIGHT.EMPTY_MERGE_WARNING(commitCount));
|
|
942
|
+
}
|
|
943
|
+
else {
|
|
944
|
+
console.log(PREFLIGHT.EMPTY_MERGE_CHECK);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
catch (e) {
|
|
948
|
+
// Re-throw validation errors (WU-1460 blocker)
|
|
949
|
+
if (e.code === ErrorCodes.VALIDATION_ERROR)
|
|
950
|
+
throw e;
|
|
951
|
+
console.warn(`${LOG_PREFIX.DONE} Warning: Could not check for empty merge: ${e.message}`);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* Check if branch is already merged to main
|
|
956
|
+
*
|
|
957
|
+
* @param {string} branch - Lane branch name
|
|
958
|
+
* @returns {Promise<boolean>} Whether branch is already merged
|
|
959
|
+
*/
|
|
960
|
+
/** @constant {number} SHA_SHORT_LENGTH - Length of shortened git SHA hashes for display */
|
|
961
|
+
const SHA_SHORT_LENGTH = 8;
|
|
962
|
+
export async function isBranchAlreadyMerged(branch) {
|
|
963
|
+
const gitAdapter = getGitForCwd();
|
|
964
|
+
try {
|
|
965
|
+
const branchTip = (await gitAdapter.getCommitHash(branch)).trim();
|
|
966
|
+
const mergeBase = (await gitAdapter.mergeBase(BRANCHES.MAIN, branch)).trim();
|
|
967
|
+
const mainHead = (await gitAdapter.getCommitHash(BRANCHES.MAIN)).trim();
|
|
968
|
+
if (branchTip === mergeBase) {
|
|
969
|
+
console.log(PREFLIGHT.BRANCH_INFO(branch, branchTip.substring(0, SHA_SHORT_LENGTH), mergeBase.substring(0, SHA_SHORT_LENGTH), mainHead.substring(0, SHA_SHORT_LENGTH)));
|
|
970
|
+
return true;
|
|
971
|
+
}
|
|
972
|
+
return false;
|
|
973
|
+
}
|
|
974
|
+
catch (e) {
|
|
975
|
+
console.warn(`${LOG_PREFIX.DONE} Warning: Could not check if branch is merged: ${e.message}`);
|
|
976
|
+
return false;
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
async function isMainAncestorOfBranch(gitAdapter, branch) {
|
|
980
|
+
try {
|
|
981
|
+
await gitAdapter.raw([GIT_COMMANDS.MERGE_BASE, GIT_FLAGS.IS_ANCESTOR, BRANCHES.MAIN, branch]);
|
|
982
|
+
return true;
|
|
983
|
+
}
|
|
984
|
+
catch {
|
|
985
|
+
return false;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
export async function mergeLaneBranch(branch, options = {}) {
|
|
989
|
+
const gitAdapter = getGitForCwd();
|
|
990
|
+
console.log(MERGE.BRANCH_MERGE(branch));
|
|
991
|
+
// WU-1747: Use exponential backoff retry for merge operations
|
|
992
|
+
// WU-1749 Bug 2: Now rebases lane branch on retry instead of just pulling main
|
|
993
|
+
const retryConfig = createRetryConfig('wu_done', {
|
|
994
|
+
maxAttempts: options.maxAttempts,
|
|
995
|
+
onRetry: async (attempt, error, delay) => {
|
|
996
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Merge attempt ${attempt} failed: ${error.message}`);
|
|
997
|
+
// WU-1749 Bug 2: Rebase lane branch onto new main instead of just pulling
|
|
998
|
+
// This is required because ff-only merge will always fail if the lane branch
|
|
999
|
+
// is still based on the old main after main has advanced
|
|
1000
|
+
if (options.worktreePath) {
|
|
1001
|
+
const mainIsAncestor = await isMainAncestorOfBranch(gitAdapter, branch);
|
|
1002
|
+
if (mainIsAncestor) {
|
|
1003
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.INFO} Main is already an ancestor - skipping auto-rebase`);
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.INFO} Auto-rebasing lane branch onto latest main...`);
|
|
1007
|
+
const rebaseResult = await autoRebaseBranch(branch, options.worktreePath, options.wuId);
|
|
1008
|
+
if (rebaseResult.success) {
|
|
1009
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Lane branch rebased - ff-only merge should succeed`);
|
|
1010
|
+
}
|
|
1011
|
+
else {
|
|
1012
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Auto-rebase failed: ${rebaseResult.error}`);
|
|
1013
|
+
// Fall back to pulling main (won't help ff-only but maintains old behaviour)
|
|
1014
|
+
try {
|
|
1015
|
+
await gitAdapter.pull(REMOTES.ORIGIN, BRANCHES.MAIN);
|
|
1016
|
+
console.log(MERGE.UPDATED_MAIN(REMOTES.ORIGIN));
|
|
1017
|
+
}
|
|
1018
|
+
catch (pullErr) {
|
|
1019
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Pull also failed: ${pullErr.message}`);
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
else {
|
|
1024
|
+
// No worktree path - fall back to old behaviour (pull only)
|
|
1025
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.INFO} Pulling latest main before retry...`);
|
|
1026
|
+
try {
|
|
1027
|
+
await gitAdapter.pull(REMOTES.ORIGIN, BRANCHES.MAIN);
|
|
1028
|
+
console.log(MERGE.UPDATED_MAIN(REMOTES.ORIGIN));
|
|
1029
|
+
}
|
|
1030
|
+
catch (pullErr) {
|
|
1031
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Pull failed: ${pullErr.message} - will retry anyway`);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
},
|
|
1035
|
+
});
|
|
1036
|
+
try {
|
|
1037
|
+
await withRetry(async () => {
|
|
1038
|
+
await gitAdapter.merge(branch, { ffOnly: true });
|
|
1039
|
+
}, retryConfig);
|
|
1040
|
+
console.log(MERGE.SUCCESS(branch));
|
|
1041
|
+
}
|
|
1042
|
+
catch (e) {
|
|
1043
|
+
// All retries exhausted
|
|
1044
|
+
const mainIsAncestor = await isMainAncestorOfBranch(gitAdapter, branch);
|
|
1045
|
+
const message = mainIsAncestor
|
|
1046
|
+
? MERGE.FF_FAILED_NON_DIVERGED_ERROR(branch, e.message)
|
|
1047
|
+
: MERGE.FF_DIVERGED_ERROR(branch, e.message);
|
|
1048
|
+
throw createError(ErrorCodes.GIT_ERROR, message, {
|
|
1049
|
+
branch,
|
|
1050
|
+
originalError: e.message,
|
|
1051
|
+
retriesExhausted: true,
|
|
1052
|
+
mainIsAncestor,
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
/**
|
|
1057
|
+
* WU-1943: Check if the session has checkpoints for the given WU
|
|
1058
|
+
*
|
|
1059
|
+
* Used to warn agents when they're completing a WU without any checkpoints,
|
|
1060
|
+
* which means no recovery data if the session crashes.
|
|
1061
|
+
*
|
|
1062
|
+
* @param {string} wuId - WU ID to check
|
|
1063
|
+
* @param {Array|null} nodes - Memory nodes for the WU (from queryByWu)
|
|
1064
|
+
* @returns {boolean} True if checkpoints exist, false otherwise
|
|
1065
|
+
*/
|
|
1066
|
+
export function hasSessionCheckpoints(wuId, nodes) {
|
|
1067
|
+
if (!nodes || !Array.isArray(nodes) || nodes.length === 0) {
|
|
1068
|
+
return false;
|
|
1069
|
+
}
|
|
1070
|
+
return nodes.some((node) => node.type === 'checkpoint');
|
|
1071
|
+
}
|
|
1072
|
+
/**
|
|
1073
|
+
* WU-1943: Rollback branch to pre-commit SHA when merge fails
|
|
1074
|
+
*
|
|
1075
|
+
* When wu:done commits metadata to the lane branch but the subsequent merge
|
|
1076
|
+
* to main fails, this function rolls back the branch to its pre-commit state.
|
|
1077
|
+
* This prevents "zombie" states where the branch shows done but wasn't merged.
|
|
1078
|
+
*
|
|
1079
|
+
* @param {object} gitAdapter - Git adapter instance (must be in worktree context)
|
|
1080
|
+
* @param {string} preCommitSha - SHA to reset to (captured before metadata commit)
|
|
1081
|
+
* @param {string} wuId - WU ID for logging
|
|
1082
|
+
* @returns {Promise<{success: boolean, error?: string}>} Rollback result
|
|
1083
|
+
*/
|
|
1084
|
+
export async function rollbackBranchOnMergeFailure(gitAdapter, preCommitSha, wuId) {
|
|
1085
|
+
try {
|
|
1086
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} WU-1943: Rolling back ${wuId} branch to pre-commit state...`);
|
|
1087
|
+
// WU-2236: GitAdapter.reset expects (ref: string, options?: { hard?: boolean })
|
|
1088
|
+
// NOT an array like ['--hard', sha]
|
|
1089
|
+
await gitAdapter.reset(preCommitSha, { hard: true });
|
|
1090
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} WU-1943: Branch rollback complete for ${wuId}`);
|
|
1091
|
+
return { success: true };
|
|
1092
|
+
}
|
|
1093
|
+
catch (error) {
|
|
1094
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} WU-1943: Could not rollback branch for ${wuId}: ${error.message}`);
|
|
1095
|
+
return { success: false, error: error.message };
|
|
1096
|
+
}
|
|
1097
|
+
}
|