@webpieces/pr-gate 0.3.354 → 0.3.355

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/package.json +7 -2
  2. package/src/dashboard/dashboard.d.ts +12 -3
  3. package/src/dashboard/dashboard.js +136 -129
  4. package/src/dashboard/dashboard.js.map +1 -1
  5. package/src/index.d.ts +2 -1
  6. package/src/index.js +4 -4
  7. package/src/index.js.map +1 -1
  8. package/src/scripts/commands/finish-update-command.d.ts +10 -0
  9. package/src/scripts/commands/finish-update-command.js +51 -0
  10. package/src/scripts/commands/finish-update-command.js.map +1 -0
  11. package/src/scripts/commands/finish-upsert-pr-command.d.ts +22 -0
  12. package/src/scripts/commands/finish-upsert-pr-command.js +131 -0
  13. package/src/scripts/commands/finish-upsert-pr-command.js.map +1 -0
  14. package/src/scripts/commands/start-update-command.d.ts +9 -0
  15. package/src/scripts/commands/start-update-command.js +67 -0
  16. package/src/scripts/commands/start-update-command.js.map +1 -0
  17. package/src/scripts/commands/start-upsert-pr-command.d.ts +15 -0
  18. package/src/scripts/commands/start-upsert-pr-command.js +73 -0
  19. package/src/scripts/commands/start-upsert-pr-command.js.map +1 -0
  20. package/src/scripts/git-finishUpsertPr.d.ts +1 -1
  21. package/src/scripts/git-finishUpsertPr.js +11 -109
  22. package/src/scripts/git-finishUpsertPr.js.map +1 -1
  23. package/src/scripts/git-gatherInfo.d.ts +15 -1
  24. package/src/scripts/git-gatherInfo.js +79 -61
  25. package/src/scripts/git-gatherInfo.js.map +1 -1
  26. package/src/scripts/pr-gate-app.d.ts +25 -0
  27. package/src/scripts/pr-gate-app.js +56 -0
  28. package/src/scripts/pr-gate-app.js.map +1 -0
  29. package/src/scripts/workflow/branch-naming.d.ts +11 -8
  30. package/src/scripts/workflow/branch-naming.js +30 -21
  31. package/src/scripts/workflow/branch-naming.js.map +1 -1
  32. package/src/scripts/workflow/build-affected.d.ts +21 -30
  33. package/src/scripts/workflow/build-affected.js +57 -63
  34. package/src/scripts/workflow/build-affected.js.map +1 -1
  35. package/src/scripts/workflow/cleanTmp.d.ts +6 -1
  36. package/src/scripts/workflow/cleanTmp.js +83 -78
  37. package/src/scripts/workflow/cleanTmp.js.map +1 -1
  38. package/src/scripts/workflow/git-exec.d.ts +32 -28
  39. package/src/scripts/workflow/git-exec.js +92 -85
  40. package/src/scripts/workflow/git-exec.js.map +1 -1
  41. package/src/scripts/workflow/git-findForkPoint.d.ts +12 -3
  42. package/src/scripts/workflow/git-findForkPoint.js +116 -107
  43. package/src/scripts/workflow/git-findForkPoint.js.map +1 -1
  44. package/src/scripts/workflow/git-readAiBranchName.d.ts +7 -2
  45. package/src/scripts/workflow/git-readAiBranchName.js +24 -15
  46. package/src/scripts/workflow/git-readAiBranchName.js.map +1 -1
  47. package/src/scripts/workflow/merge-end.d.ts +21 -6
  48. package/src/scripts/workflow/merge-end.js +147 -133
  49. package/src/scripts/workflow/merge-end.js.map +1 -1
  50. package/src/scripts/workflow/merge-start.d.ts +22 -1
  51. package/src/scripts/workflow/merge-start.js +180 -186
  52. package/src/scripts/workflow/merge-start.js.map +1 -1
  53. package/src/scripts/workflow/merge-state.d.ts +26 -23
  54. package/src/scripts/workflow/merge-state.js +131 -137
  55. package/src/scripts/workflow/merge-state.js.map +1 -1
  56. package/src/scripts/workflow/open-pr-check.d.ts +7 -1
  57. package/src/scripts/workflow/open-pr-check.js +34 -23
  58. package/src/scripts/workflow/open-pr-check.js.map +1 -1
  59. package/src/scripts/workflow/run-update.d.ts +13 -1
  60. package/src/scripts/workflow/run-update.js +59 -28
  61. package/src/scripts/workflow/run-update.js.map +1 -1
  62. package/src/scripts/wp-finish-update.d.ts +1 -1
  63. package/src/scripts/wp-finish-update.js +11 -30
  64. package/src/scripts/wp-finish-update.js.map +1 -1
  65. package/src/scripts/wp-start-update.d.ts +1 -2
  66. package/src/scripts/wp-start-update.js +11 -51
  67. package/src/scripts/wp-start-update.js.map +1 -1
  68. package/src/scripts/wp-start-upsert-pr.d.ts +1 -1
  69. package/src/scripts/wp-start-upsert-pr.js +11 -47
  70. package/src/scripts/wp-start-upsert-pr.js.map +1 -1
@@ -1,24 +1,17 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.MergeStartResult = exports.MergeContext = void 0;
4
- exports.mergeStart = mergeStart;
3
+ exports.MergeStart = exports.MergeStartResult = exports.MergeContext = void 0;
5
4
  const tslib_1 = require("tslib");
6
5
  const child_process_1 = require("child_process");
7
6
  const fs = tslib_1.__importStar(require("fs"));
8
7
  const path = tslib_1.__importStar(require("path"));
9
8
  const rules_config_1 = require("@webpieces/rules-config");
9
+ const core_context_1 = require("@webpieces/core-context");
10
+ const inversify_1 = require("inversify");
10
11
  const git_gatherInfo_1 = require("../git-gatherInfo");
11
12
  const branch_naming_1 = require("./branch-naming");
12
13
  const git_exec_1 = require("./git-exec");
13
14
  const merge_state_1 = require("./merge-state");
14
- // merge-START: the first half of the 3-point squash-merge lifecycle. Brings origin/main into a fresh
15
- // `<branch>Squash`, and on conflict writes the 3-point context files + the unvalidated marker + the
16
- // process doc, then hands control back to the AI. On a clean merge it commits the squash and returns
17
- // the branch context so the caller (runUpdateFromMain) can run merge-END to finalize. It NEVER
18
- // finalizes or posts a PR — that is merge-END's / wp-finish-upsert-pr's job. Shared so
19
- // runUpdateFromMain (and, via it, wp-start-update + wp-start-upsert-pr) all set up a merge through
20
- // one code path. `finishCommand` names the command the AI runs to finish after resolving conflicts
21
- // (standalone update → `wp-finish-update`; PR flow → `wp-finish-upsert-pr`).
22
15
  const SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
23
16
  // The four branch names merge-END needs to finalize a merge (swap squash→feature, push, clean up).
24
17
  class MergeContext {
@@ -47,12 +40,6 @@ class MergeStartResult {
47
40
  }
48
41
  }
49
42
  exports.MergeStartResult = MergeStartResult;
50
- // Detect the PR by its STABLE feature branch — pass baseBranchName(currentBranch) so a leftover
51
- // `…wpN` from the old scheme still resolves to the one name the PR lives on.
52
- function detectPr(baseBranch) {
53
- const result = (0, child_process_1.spawnSync)('gh', ['pr', 'list', '--head', baseBranch, '--json', 'number', '--jq', '.[0].number'], { encoding: 'utf8' });
54
- return result.status === 0 ? (result.stdout ?? '').trim() : '';
55
- }
56
43
  // The one number `n` for a sync and the two things it names: the pre-merge backup branch and its
57
44
  // paired conflict-context run dir.
58
45
  class SyncSlot {
@@ -63,48 +50,8 @@ class SyncSlot {
63
50
  this.runDir = runDir;
64
51
  }
65
52
  }
66
- // Pick this sync's slot number MONOTONICALLY from the durable audit dirs one past the highest
67
- // existing `<home>/merge-<n>/`, then stepped past any existing `<branch>PreMerge<n>` branch so the
68
- // same `n` is also free for the paired backup branch. Unlike the old first-free-branch scheme, the
69
- // number is never recycled onto a prior merge's context, so we DO NOT (and must not) wipe the run
70
- // dir: earlier `merge-<k>/` dirs are the audit trail we now deliberately keep. Returns the backup
71
- // branch name + the (fresh, non-existent) run dir path, both keyed on `n`.
72
- function chooseSyncSlot(home, currentBranch) {
73
- const branchExists = (name) => (0, child_process_1.spawnSync)('git', ['show-ref', '--verify', '--quiet', `refs/heads/${name}`]).status === 0;
74
- let n = (0, merge_state_1.nextMergeSlotNumber)(home);
75
- while (branchExists((0, branch_naming_1.preMergeBackupName)(currentBranch, n)))
76
- n += 1;
77
- return new SyncSlot((0, branch_naming_1.preMergeBackupName)(currentBranch, n), (0, merge_state_1.mergeRunDirFor)(home, n));
78
- }
79
- // Snapshot the pre-merge state onto the caller-chosen `backupBranch` (`<currentBranch>PreMerge<n>`),
80
- // never overwriting. The slot number `n` is picked once in mergeStart and shared with the paired
81
- // `merge-<n>/` context dir. A clean sync deletes this snapshot at finalize; only conflict syncs keep it.
82
- function createBackup(currentBranch, backupBranch) {
83
- process.stdout.write('\n' + SEP + '💾 Creating Pre-Merge Backup\n' + SEP + '\n');
84
- (0, git_exec_1.runGitChecked)(['checkout', '-b', backupBranch], 'Failed to create backup branch');
85
- (0, git_exec_1.runGitChecked)(['checkout', currentBranch], 'Failed to return to feature branch');
86
- process.stdout.write(`✅ Backup created: ${backupBranch}\n\n`);
87
- }
88
- function saveConflictContext(conflictedFiles, mergeDir, forkPoint, featureHead, mainHead) {
89
- for (const file of conflictedFiles) {
90
- const fileDir = (0, merge_state_1.perFileContextDir)(mergeDir, file);
91
- fs.mkdirSync(fileDir, { recursive: true });
92
- const fork = (0, child_process_1.spawnSync)('git', ['show', `${forkPoint}:${file}`], { encoding: 'utf8' });
93
- fs.writeFileSync(path.join(fileDir, 'A-forkpoint.txt'), fork.status === 0 ? (fork.stdout ?? '') : '(file did not exist)\n');
94
- const feature = (0, child_process_1.spawnSync)('git', ['show', `${featureHead}:${file}`], { encoding: 'utf8' });
95
- fs.writeFileSync(path.join(fileDir, 'B-feature.txt'), feature.status === 0 ? (feature.stdout ?? '') : '(file did not exist)\n');
96
- const main = (0, child_process_1.spawnSync)('git', ['show', `${mainHead}:${file}`], { encoding: 'utf8' });
97
- fs.writeFileSync(path.join(fileDir, 'C-main.txt'), main.status === 0 ? (main.stdout ?? '') : '(file did not exist)\n');
98
- const ba = (0, child_process_1.spawnSync)('git', ['diff', forkPoint, featureHead, '--', file], { encoding: 'utf8' });
99
- fs.writeFileSync(path.join(fileDir, 'B-A.diff'), ba.stdout ?? '');
100
- const ca = (0, child_process_1.spawnSync)('git', ['diff', forkPoint, mainHead, '--', file], { encoding: 'utf8' });
101
- fs.writeFileSync(path.join(fileDir, 'C-A.diff'), ca.stdout ?? '');
102
- }
103
- }
104
- // Single source of truth for the merge process. The script WRITES it at conflict time (rather
105
- // than the AI relying on a separate hand-maintained doc), parameterized with the live MERGE_DIR
106
- // and conflicted-file list, so the instructions can never drift from the actual layout. The body
107
- // is a template constant with {{...}} placeholders so this stays a small filler function.
53
+ // Single source of truth for the merge process (written at conflict time, parameterized with the live
54
+ // MERGE_DIR + conflicted-file list so it can never drift from the actual layout).
108
55
  const MERGE_PROCESS_TEMPLATE = `# AI-Assisted Squash-Merge Conflict Resolution (generated)
109
56
 
110
57
  This file was generated by \`pnpm wp-start-update\` when the 3-point squash-merge hit conflicts.
@@ -188,138 +135,185 @@ git merge --abort 2>/dev/null; git checkout <feature> ; git branch -D {{SQUASH_B
188
135
 
189
136
  Then delete \`{{MERGE_DIR}}/\` for a clean slate.
190
137
  `;
191
- function mergeProcessDoc(mergeDir, squashBranch, conflictedFiles, finishCommand) {
192
- const fileList = conflictedFiles.map((f) => `- \`${f}\``).join('\n');
193
- return MERGE_PROCESS_TEMPLATE
194
- .replace(/\{\{SQUASH_BRANCH\}\}/g, squashBranch)
195
- .replace(/\{\{MERGE_DIR\}\}/g, mergeDir)
196
- .replace(/\{\{EXPLANATION_FILE\}\}/g, rules_config_1.MERGE_EXPLANATION_FILE)
197
- .replace(/\{\{FINISH_COMMAND\}\}/g, finishCommand)
198
- .replace(/\{\{FILE_LIST\}\}/g, fileList);
199
- }
200
- // Returns the absolute path of the written doc.
201
- function writeMergeProcessDoc(repoRoot, mergeDir, squashBranch, conflictedFiles, finishCommand) {
202
- const docDir = path.join(repoRoot, rules_config_1.WEBPIECES_TMP_DIR, 'instruct-ai');
203
- fs.mkdirSync(docDir, { recursive: true });
204
- const docPath = path.join(docDir, 'webpieces.mergeprocess.md');
205
- fs.writeFileSync(docPath, mergeProcessDoc(mergeDir, squashBranch, conflictedFiles, finishCommand));
206
- return docPath;
207
- }
208
- // The AI-facing "what just happened / what to do next" recap on the conflict path. Explicit and
209
- // numbered so an agent (or human) can't miss where the context lives or which command finishes.
210
- function printConflictHandback(docPath, mergeDir, squashBranch, conflictedFiles, finishCommand) {
211
- process.stdout.write('\n' + SEP + `⚠️ Conflicts in ${conflictedFiles.length} file(s) — handing control back to you\n` + SEP + '\n');
212
- process.stdout.write('Here is exactly what I did and what you need to do:\n\n');
213
- process.stdout.write('What I did:\n');
214
- process.stdout.write(' 1. snapshotted your pre-merge state to a PreMerge branch\n');
215
- process.stdout.write(' 2. pulled origin/main and squash-merged your work onto it\n');
216
- process.stdout.write(` 3. hit conflicts — you are now on the transient branch ${squashBranch}\n\n`);
217
- process.stdout.write('What you need to do:\n');
218
- process.stdout.write(` 1. read the merge process doc: ${docPath}\n`);
219
- process.stdout.write(` 2. resolve each conflicted file below (its 3-point A/B/C context + diffs are in\n`);
220
- process.stdout.write(` ${mergeDir}/updatemain-<file>/), and write that file's merge-explanation.md\n`);
221
- process.stdout.write(` 3. run pnpm ${finishCommand} — it validates, commits, and finalizes (do NOT git add/commit/push yourself)\n\n`);
222
- process.stdout.write('Conflicted files:\n');
223
- for (const file of conflictedFiles)
224
- process.stdout.write(` - ${file}\n`);
225
- process.stdout.write('\n' + SEP);
226
- }
227
- // Write the conflict context files + the unvalidated marker + the process doc. Does NOT exit — the
228
- // caller decides (runUpdateFromMain exits 2 to hand back to the AI).
229
- function handleConflictsHandback(repoRoot, mergeDir, currentBranch, squashBranch, backupBranch, prNumber, hashes, finishCommand) {
230
- const raw = (0, child_process_1.execSync)('git diff --name-only --diff-filter=U', { encoding: 'utf8' }).trim();
231
- const conflictedFiles = raw.split('\n').filter((f) => f.trim() !== '');
232
- fs.mkdirSync(mergeDir, { recursive: true }); // run dir was wiped at sync start — create it fresh
233
- fs.writeFileSync(path.join(mergeDir, 'updatemain-conflicted-files.txt'), raw + '\n');
234
- // Copy the A/B/C hashes into THIS run dir so the merge-process doc's `MERGE_DIR/updatemain-hashes.json`
235
- // pointer is accurate (gatherInfo writes the source copy into the feature home before the slot is known).
236
- fs.writeFileSync(path.join(mergeDir, 'updatemain-hashes.json'), JSON.stringify(hashes, null, 2) + '\n');
237
- saveConflictContext(conflictedFiles, mergeDir, hashes.hashForkPoint, hashes.hashFeatureHead, hashes.hashMainHead);
238
- const marker = new merge_state_1.MergeMarker(currentBranch, squashBranch, backupBranch, prNumber, conflictedFiles, hashes.hashForkPoint, hashes.hashFeatureHead, hashes.hashMainHead, false);
239
- (0, merge_state_1.writeMergeMarker)(mergeDir, marker);
240
- const docPath = writeMergeProcessDoc(repoRoot, mergeDir, squashBranch, conflictedFiles, finishCommand);
241
- printConflictHandback(docPath, mergeDir, squashBranch, conflictedFiles, finishCommand);
242
- }
243
- // Short sha of a ref (best-effort — '' if it can't resolve), for the branch-mutation log's
244
- // oldMain→newMain annotation.
245
- function shortSha(ref) {
246
- const result = (0, child_process_1.spawnSync)('git', ['rev-parse', '--short', ref], { encoding: 'utf8' });
247
- return result.status === 0 ? (result.stdout ?? '').trim() : '';
248
- }
249
- // Log the CONFLICT phase with the conflicted-file list + the artifact paths a resolver needs — the
250
- // per-file 3-point context dir and the generated merge-process doc — so the branch-mutation log alone
251
- // tells the next agent where to look (no reflog/log archaeology).
252
- function logConflict(repoRoot, verb, mergeDir) {
253
- const raw = (0, child_process_1.spawnSync)('git', ['diff', '--name-only', '--diff-filter=U'], { cwd: repoRoot, encoding: 'utf8' });
254
- const files = (raw.status === 0 ? (raw.stdout ?? '') : '').split('\n').map((f) => f.trim()).filter((f) => f !== '');
255
- const event = new rules_config_1.BranchMutationEvent(verb, 'CONFLICT');
256
- event.conflict = true;
257
- event.conflictFiles = files;
258
- event.artifacts = [
259
- path.join(mergeDir, 'updatemain-<file>'),
260
- path.join(repoRoot, rules_config_1.WEBPIECES_TMP_DIR, 'instruct-ai', 'webpieces.mergeprocess.md'),
261
- ];
262
- (0, rules_config_1.logBranchMutation)(repoRoot, event);
263
- }
264
- async function mergeStart(repoRoot, verb, home, finishCommand) {
265
- const currentBranch = (0, child_process_1.execSync)('git branch --show-current', { encoding: 'utf8' }).trim();
266
- if (currentBranch.endsWith('Squash')) {
267
- throw new rules_config_1.CliExitError(1, `❌ On a leftover ${currentBranch} branch with no merge marker. Clean up: git branch -D ${currentBranch}`);
138
+ // merge-START: the first half of the 3-point squash-merge lifecycle. Brings origin/main into a fresh
139
+ // `<branch>Squash`, and on conflict writes the 3-point context + unvalidated marker + process doc,
140
+ // then hands control back to the AI. On a clean merge it commits the squash and returns the branch
141
+ // context so the caller (RunUpdate) can run merge-END to finalize. NEVER finalizes or posts a PR.
142
+ let MergeStart = class MergeStart {
143
+ gatherInfo;
144
+ branchNaming;
145
+ gitExec;
146
+ mergeState;
147
+ constructor(gatherInfo, branchNaming, gitExec, mergeState) {
148
+ this.gatherInfo = gatherInfo;
149
+ this.branchNaming = branchNaming;
150
+ this.gitExec = gitExec;
151
+ this.mergeState = mergeState;
268
152
  }
269
- // One number `n` for this sync drives BOTH the backup branch and its `merge-<n>/` context dir.
270
- const slot = chooseSyncSlot(home, currentBranch);
271
- const backupBranch = slot.backupBranch;
272
- const mergeDir = slot.runDir;
273
- process.stdout.write('\n' + SEP + '🔄 Squash-Merge Update from Main\n' + SEP + '\n');
274
- // gatherInfo RETURNS (never exits): an already-even-with-main branch is NOT special-cased here
275
- // we flow straight on. The squash merge below becomes a no-op that the `nothingStaged` path
276
- // (further down) handles, so the caller still proceeds to push + build. (Previously gatherInfo
277
- // process.exit(0)'d on this case, silently killing wp-start-upsert-pr before push/build.)
278
- const info = await (0, git_gatherInfo_1.gatherInfo)();
279
- const hashes = info.hashes;
280
- if (info.alreadyUpToDate) {
281
- process.stdout.write('ℹ️ Branch already even with main; nothing to merge, continuing to push/build.\n');
153
+ async mergeStart(repoRoot, verb, home, finishCommand) {
154
+ const currentBranch = (0, child_process_1.execSync)('git branch --show-current', { encoding: 'utf8' }).trim();
155
+ if (currentBranch.endsWith('Squash')) {
156
+ throw new rules_config_1.CliExitError(1, `❌ On a leftover ${currentBranch} branch with no merge marker. Clean up: git branch -D ${currentBranch}`);
157
+ }
158
+ // One number `n` for this sync drives BOTH the backup branch and its `merge-<n>/` context dir.
159
+ const slot = this.chooseSyncSlot(home, currentBranch);
160
+ const backupBranch = slot.backupBranch;
161
+ const mergeDir = slot.runDir;
162
+ process.stdout.write('\n' + SEP + '🔄 Squash-Merge Update from Main\n' + SEP + '\n');
163
+ const info = await this.gatherInfo.gatherInfo();
164
+ const hashes = info.hashes;
165
+ if (info.alreadyUpToDate) {
166
+ process.stdout.write('ℹ️ Branch already even with main; nothing to merge, continuing to push/build.\n');
167
+ }
168
+ const prNumber = this.detectPr(this.branchNaming.baseBranchName(currentBranch));
169
+ process.stdout.write(prNumber ? `Existing PR #${prNumber} will be updated.\n` : 'No existing PR (one can be created later).\n');
170
+ this.createBackup(currentBranch, backupBranch);
171
+ const backupEvent = new rules_config_1.BranchMutationEvent(verb, 'BACKUP');
172
+ backupEvent.fromBranch = currentBranch;
173
+ backupEvent.toBranch = backupBranch;
174
+ (0, rules_config_1.logBranchMutation)(repoRoot, backupEvent);
175
+ const squashBranch = `${currentBranch}Squash`;
176
+ if ((0, child_process_1.spawnSync)('git', ['show-ref', '--verify', '--quiet', `refs/heads/${squashBranch}`]).status === 0) {
177
+ throw new rules_config_1.CliExitError(1, `❌ Stale ${squashBranch} from a previous run. Delete it: git branch -D ${squashBranch}`);
178
+ }
179
+ // Branch the squash off origin/main directly — worktree-native. origin/main was already fetched
180
+ // in gatherInfo(), and A/B/C are computed purely from origin/main, so the merge base is identical
181
+ // to the old checkout-main + pull path.
182
+ const originMainSha = this.shortSha('origin/main');
183
+ this.gitExec.runGitChecked(['checkout', '-b', squashBranch, 'origin/main'], 'Failed to create squash branch off origin/main');
184
+ const baseEvent = new rules_config_1.BranchMutationEvent(verb, 'PULL');
185
+ baseEvent.newMain = originMainSha;
186
+ (0, rules_config_1.logBranchMutation)(repoRoot, baseEvent);
187
+ process.stdout.write('\n' + SEP + `🔀 Squash merging ${currentBranch}\n` + SEP + '\n');
188
+ const merge = (0, child_process_1.spawnSync)('git', ['merge', '--squash', currentBranch], { stdio: 'inherit' });
189
+ if (merge.status !== 0) {
190
+ this.handleConflictsHandback(repoRoot, mergeDir, currentBranch, squashBranch, backupBranch, prNumber, hashes, finishCommand);
191
+ this.logConflict(repoRoot, verb, mergeDir);
192
+ return new MergeStartResult('conflict', null, mergeDir);
193
+ }
194
+ (0, rules_config_1.logBranchMutation)(repoRoot, new rules_config_1.BranchMutationEvent(verb, 'SQUASH'));
195
+ const nothingStaged = (0, child_process_1.spawnSync)('git', ['diff-index', '--quiet', '--cached', 'HEAD', '--']).status === 0;
196
+ if (nothingStaged) {
197
+ process.stdout.write('ℹ️ Already up-to-date with main (nothing to merge).\n');
198
+ }
199
+ else {
200
+ this.gitExec.runGitChecked(['commit', '-m', `Squash merge of ${currentBranch}`], 'Failed to commit squash merge');
201
+ this.mergeState.writeCleanMergeMarker(mergeDir, hashes.hashForkPoint, hashes.hashFeatureHead, hashes.hashMainHead);
202
+ }
203
+ return new MergeStartResult('clean', new MergeContext(currentBranch, squashBranch, backupBranch, prNumber), mergeDir);
282
204
  }
283
- const prNumber = detectPr((0, branch_naming_1.baseBranchName)(currentBranch));
284
- process.stdout.write(prNumber ? `Existing PR #${prNumber} will be updated.\n` : 'No existing PR (one can be created later).\n');
285
- createBackup(currentBranch, backupBranch);
286
- const backupEvent = new rules_config_1.BranchMutationEvent(verb, 'BACKUP');
287
- backupEvent.fromBranch = currentBranch;
288
- backupEvent.toBranch = backupBranch;
289
- (0, rules_config_1.logBranchMutation)(repoRoot, backupEvent);
290
- const squashBranch = `${currentBranch}Squash`;
291
- if ((0, child_process_1.spawnSync)('git', ['show-ref', '--verify', '--quiet', `refs/heads/${squashBranch}`]).status === 0) {
292
- throw new rules_config_1.CliExitError(1, `❌ Stale ${squashBranch} from a previous run. Delete it: git branch -D ${squashBranch}`);
205
+ // Detect the PR by its STABLE feature branch (a leftover `…wpN` still resolves to the one name the
206
+ // PR lives on).
207
+ detectPr(baseBranch) {
208
+ const result = (0, child_process_1.spawnSync)('gh', ['pr', 'list', '--head', baseBranch, '--json', 'number', '--jq', '.[0].number'], { encoding: 'utf8' });
209
+ return result.status === 0 ? (result.stdout ?? '').trim() : '';
293
210
  }
294
- // Branch the squash off origin/main directly worktree-native. Never checks out or mutates local
295
- // `main`, which fatals inside a linked worktree (`git checkout main` "'main' is already checked
296
- // out at <primary>"). origin/main was already fetched in gatherInfo() above, and the fork point +
297
- // all three hash points (A/B/C) are computed purely from origin/main, so the squash merge base is
298
- // identical to the old checkout-main + pull path — this is a behavior-preserving change on the
299
- // primary repo and an unblock inside worktrees.
300
- const originMainSha = shortSha('origin/main');
301
- (0, git_exec_1.runGitChecked)(['checkout', '-b', squashBranch, 'origin/main'], 'Failed to create squash branch off origin/main');
302
- const baseEvent = new rules_config_1.BranchMutationEvent(verb, 'PULL');
303
- baseEvent.newMain = originMainSha;
304
- (0, rules_config_1.logBranchMutation)(repoRoot, baseEvent);
305
- process.stdout.write('\n' + SEP + `🔀 Squash merging ${currentBranch}\n` + SEP + '\n');
306
- const merge = (0, child_process_1.spawnSync)('git', ['merge', '--squash', currentBranch], { stdio: 'inherit' });
307
- if (merge.status !== 0) {
308
- handleConflictsHandback(repoRoot, mergeDir, currentBranch, squashBranch, backupBranch, prNumber, hashes, finishCommand);
309
- logConflict(repoRoot, verb, mergeDir);
310
- return new MergeStartResult('conflict', null, mergeDir);
211
+ // Pick this sync's slot number MONOTONICALLY from the durable audit dirs, then step past any
212
+ // existing `<branch>PreMerge<n>` branch so the same `n` is free for the paired backup branch.
213
+ chooseSyncSlot(home, currentBranch) {
214
+ const branchExists = (name) => (0, child_process_1.spawnSync)('git', ['show-ref', '--verify', '--quiet', `refs/heads/${name}`]).status === 0;
215
+ let n = this.mergeState.nextMergeSlotNumber(home);
216
+ while (branchExists(this.branchNaming.preMergeBackupName(currentBranch, n)))
217
+ n += 1;
218
+ return new SyncSlot(this.branchNaming.preMergeBackupName(currentBranch, n), this.mergeState.mergeRunDirFor(home, n));
311
219
  }
312
- (0, rules_config_1.logBranchMutation)(repoRoot, new rules_config_1.BranchMutationEvent(verb, 'SQUASH'));
313
- const nothingStaged = (0, child_process_1.spawnSync)('git', ['diff-index', '--quiet', '--cached', 'HEAD', '--']).status === 0;
314
- if (nothingStaged) {
315
- process.stdout.write('ℹ️ Already up-to-date with main (nothing to merge).\n');
220
+ // Snapshot the pre-merge state onto the caller-chosen `backupBranch`, never overwriting.
221
+ createBackup(currentBranch, backupBranch) {
222
+ process.stdout.write('\n' + SEP + '💾 Creating Pre-Merge Backup\n' + SEP + '\n');
223
+ this.gitExec.runGitChecked(['checkout', '-b', backupBranch], 'Failed to create backup branch');
224
+ this.gitExec.runGitChecked(['checkout', currentBranch], 'Failed to return to feature branch');
225
+ process.stdout.write(`✅ Backup created: ${backupBranch}\n\n`);
316
226
  }
317
- else {
318
- (0, git_exec_1.runGitChecked)(['commit', '-m', `Squash merge of ${currentBranch}`], 'Failed to commit squash merge');
319
- // A real merge happened with no conflicts — leave a durable slot record so EVERY merge (not
320
- // just conflicted ones) is accounted for under merge-info/<feature>/merge-<n>/.
321
- (0, merge_state_1.writeCleanMergeMarker)(mergeDir, hashes.hashForkPoint, hashes.hashFeatureHead, hashes.hashMainHead);
227
+ saveConflictContext(conflictedFiles, mergeDir, forkPoint, featureHead, mainHead) {
228
+ for (const file of conflictedFiles) {
229
+ const fileDir = this.mergeState.perFileContextDir(mergeDir, file);
230
+ fs.mkdirSync(fileDir, { recursive: true });
231
+ const fork = (0, child_process_1.spawnSync)('git', ['show', `${forkPoint}:${file}`], { encoding: 'utf8' });
232
+ fs.writeFileSync(path.join(fileDir, 'A-forkpoint.txt'), fork.status === 0 ? (fork.stdout ?? '') : '(file did not exist)\n');
233
+ const feature = (0, child_process_1.spawnSync)('git', ['show', `${featureHead}:${file}`], { encoding: 'utf8' });
234
+ fs.writeFileSync(path.join(fileDir, 'B-feature.txt'), feature.status === 0 ? (feature.stdout ?? '') : '(file did not exist)\n');
235
+ const main = (0, child_process_1.spawnSync)('git', ['show', `${mainHead}:${file}`], { encoding: 'utf8' });
236
+ fs.writeFileSync(path.join(fileDir, 'C-main.txt'), main.status === 0 ? (main.stdout ?? '') : '(file did not exist)\n');
237
+ const ba = (0, child_process_1.spawnSync)('git', ['diff', forkPoint, featureHead, '--', file], { encoding: 'utf8' });
238
+ fs.writeFileSync(path.join(fileDir, 'B-A.diff'), ba.stdout ?? '');
239
+ const ca = (0, child_process_1.spawnSync)('git', ['diff', forkPoint, mainHead, '--', file], { encoding: 'utf8' });
240
+ fs.writeFileSync(path.join(fileDir, 'C-A.diff'), ca.stdout ?? '');
241
+ }
322
242
  }
323
- return new MergeStartResult('clean', new MergeContext(currentBranch, squashBranch, backupBranch, prNumber), mergeDir);
324
- }
243
+ mergeProcessDoc(mergeDir, squashBranch, conflictedFiles, finishCommand) {
244
+ const fileList = conflictedFiles.map((f) => `- \`${f}\``).join('\n');
245
+ return MERGE_PROCESS_TEMPLATE
246
+ .replace(/\{\{SQUASH_BRANCH\}\}/g, squashBranch)
247
+ .replace(/\{\{MERGE_DIR\}\}/g, mergeDir)
248
+ .replace(/\{\{EXPLANATION_FILE\}\}/g, rules_config_1.MERGE_EXPLANATION_FILE)
249
+ .replace(/\{\{FINISH_COMMAND\}\}/g, finishCommand)
250
+ .replace(/\{\{FILE_LIST\}\}/g, fileList);
251
+ }
252
+ // Returns the absolute path of the written doc.
253
+ writeMergeProcessDoc(repoRoot, mergeDir, squashBranch, conflictedFiles, finishCommand) {
254
+ const docDir = path.join(repoRoot, rules_config_1.WEBPIECES_TMP_DIR, 'instruct-ai');
255
+ fs.mkdirSync(docDir, { recursive: true });
256
+ const docPath = path.join(docDir, 'webpieces.mergeprocess.md');
257
+ fs.writeFileSync(docPath, this.mergeProcessDoc(mergeDir, squashBranch, conflictedFiles, finishCommand));
258
+ return docPath;
259
+ }
260
+ // The AI-facing "what just happened / what to do next" recap on the conflict path.
261
+ printConflictHandback(docPath, mergeDir, squashBranch, conflictedFiles, finishCommand) {
262
+ process.stdout.write('\n' + SEP + `⚠️ Conflicts in ${conflictedFiles.length} file(s) — handing control back to you\n` + SEP + '\n');
263
+ process.stdout.write('Here is exactly what I did and what you need to do:\n\n');
264
+ process.stdout.write('What I did:\n');
265
+ process.stdout.write(' 1. snapshotted your pre-merge state to a PreMerge branch\n');
266
+ process.stdout.write(' 2. pulled origin/main and squash-merged your work onto it\n');
267
+ process.stdout.write(` 3. hit conflicts — you are now on the transient branch ${squashBranch}\n\n`);
268
+ process.stdout.write('What you need to do:\n');
269
+ process.stdout.write(` 1. read the merge process doc: ${docPath}\n`);
270
+ process.stdout.write(` 2. resolve each conflicted file below (its 3-point A/B/C context + diffs are in\n`);
271
+ process.stdout.write(` ${mergeDir}/updatemain-<file>/), and write that file's merge-explanation.md\n`);
272
+ process.stdout.write(` 3. run pnpm ${finishCommand} — it validates, commits, and finalizes (do NOT git add/commit/push yourself)\n\n`);
273
+ process.stdout.write('Conflicted files:\n');
274
+ for (const file of conflictedFiles)
275
+ process.stdout.write(` - ${file}\n`);
276
+ process.stdout.write('\n' + SEP);
277
+ }
278
+ // Write the conflict context files + the unvalidated marker + the process doc. Does NOT exit.
279
+ handleConflictsHandback(repoRoot, mergeDir, currentBranch, squashBranch, backupBranch, prNumber, hashes, finishCommand) {
280
+ const raw = (0, child_process_1.execSync)('git diff --name-only --diff-filter=U', { encoding: 'utf8' }).trim();
281
+ const conflictedFiles = raw.split('\n').filter((f) => f.trim() !== '');
282
+ fs.mkdirSync(mergeDir, { recursive: true });
283
+ fs.writeFileSync(path.join(mergeDir, 'updatemain-conflicted-files.txt'), raw + '\n');
284
+ fs.writeFileSync(path.join(mergeDir, 'updatemain-hashes.json'), JSON.stringify(hashes, null, 2) + '\n');
285
+ this.saveConflictContext(conflictedFiles, mergeDir, hashes.hashForkPoint, hashes.hashFeatureHead, hashes.hashMainHead);
286
+ const marker = new merge_state_1.MergeMarker(currentBranch, squashBranch, backupBranch, prNumber, conflictedFiles, hashes.hashForkPoint, hashes.hashFeatureHead, hashes.hashMainHead, false);
287
+ this.mergeState.writeMergeMarker(mergeDir, marker);
288
+ const docPath = this.writeMergeProcessDoc(repoRoot, mergeDir, squashBranch, conflictedFiles, finishCommand);
289
+ this.printConflictHandback(docPath, mergeDir, squashBranch, conflictedFiles, finishCommand);
290
+ }
291
+ // Short sha of a ref (best-effort — '' if it can't resolve).
292
+ shortSha(ref) {
293
+ const result = (0, child_process_1.spawnSync)('git', ['rev-parse', '--short', ref], { encoding: 'utf8' });
294
+ return result.status === 0 ? (result.stdout ?? '').trim() : '';
295
+ }
296
+ // Log the CONFLICT phase with the conflicted-file list + artifact paths a resolver needs.
297
+ logConflict(repoRoot, verb, mergeDir) {
298
+ const raw = (0, child_process_1.spawnSync)('git', ['diff', '--name-only', '--diff-filter=U'], { cwd: repoRoot, encoding: 'utf8' });
299
+ const files = (raw.status === 0 ? (raw.stdout ?? '') : '').split('\n').map((f) => f.trim()).filter((f) => f !== '');
300
+ const event = new rules_config_1.BranchMutationEvent(verb, 'CONFLICT');
301
+ event.conflict = true;
302
+ event.conflictFiles = files;
303
+ event.artifacts = [
304
+ path.join(mergeDir, 'updatemain-<file>'),
305
+ path.join(repoRoot, rules_config_1.WEBPIECES_TMP_DIR, 'instruct-ai', 'webpieces.mergeprocess.md'),
306
+ ];
307
+ (0, rules_config_1.logBranchMutation)(repoRoot, event);
308
+ }
309
+ };
310
+ exports.MergeStart = MergeStart;
311
+ exports.MergeStart = MergeStart = tslib_1.__decorate([
312
+ (0, core_context_1.provideSingleton)(),
313
+ (0, inversify_1.injectable)(),
314
+ tslib_1.__metadata("design:paramtypes", [git_gatherInfo_1.GatherInfo,
315
+ branch_naming_1.BranchNaming,
316
+ git_exec_1.GitExec,
317
+ merge_state_1.MergeState])
318
+ ], MergeStart);
325
319
  //# sourceMappingURL=merge-start.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"merge-start.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/merge-start.ts"],"names":[],"mappings":"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","sourcesContent":["import { execSync, spawnSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport {\n WEBPIECES_TMP_DIR, MERGE_EXPLANATION_FILE, CliExitError,\n MutationVerb, BranchMutationEvent, logBranchMutation,\n} from '@webpieces/rules-config';\nimport { gatherInfo } from '../git-gatherInfo';\nimport { baseBranchName, preMergeBackupName } from './branch-naming';\nimport { runGitChecked } from './git-exec';\nimport {\n MergeMarker, perFileContextDir, writeMergeMarker, mergeRunDirFor,\n nextMergeSlotNumber, writeCleanMergeMarker,\n} from './merge-state';\n\n// merge-START: the first half of the 3-point squash-merge lifecycle. Brings origin/main into a fresh\n// `<branch>Squash`, and on conflict writes the 3-point context files + the unvalidated marker + the\n// process doc, then hands control back to the AI. On a clean merge it commits the squash and returns\n// the branch context so the caller (runUpdateFromMain) can run merge-END to finalize. It NEVER\n// finalizes or posts a PR — that is merge-END's / wp-finish-upsert-pr's job. Shared so\n// runUpdateFromMain (and, via it, wp-start-update + wp-start-upsert-pr) all set up a merge through\n// one code path. `finishCommand` names the command the AI runs to finish after resolving conflicts\n// (standalone update → `wp-finish-update`; PR flow → `wp-finish-upsert-pr`).\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\ninterface HashPoints {\n hashForkPoint: string;\n hashFeatureHead: string;\n hashMainHead: string;\n}\n\n// The four branch names merge-END needs to finalize a merge (swap squash→feature, push, clean up).\nexport class MergeContext {\n currentBranch: string;\n squashBranch: string;\n backupBranch: string;\n prNumber: string;\n\n constructor(currentBranch: string, squashBranch: string, backupBranch: string, prNumber: string) {\n this.currentBranch = currentBranch;\n this.squashBranch = squashBranch;\n this.backupBranch = backupBranch;\n this.prNumber = prNumber;\n }\n}\n\n// Outcome of merge-start: 'clean' carries the context for merge-END to finalize; 'conflict' means the\n// marker + context files were written and the caller should hand back to the AI (exit 2).\nexport class MergeStartResult {\n status: 'clean' | 'conflict';\n context: MergeContext | null;\n runDir: string; // this sync's numbered `merge-<n>/` dir — passed to merge-END so it reads THIS marker\n\n constructor(status: 'clean' | 'conflict', context: MergeContext | null, runDir: string) {\n this.status = status;\n this.context = context;\n this.runDir = runDir;\n }\n}\n\n// Detect the PR by its STABLE feature branch — pass baseBranchName(currentBranch) so a leftover\n// `…wpN` from the old scheme still resolves to the one name the PR lives on.\nfunction detectPr(baseBranch: string): string {\n const result = spawnSync(\n 'gh', ['pr', 'list', '--head', baseBranch, '--json', 'number', '--jq', '.[0].number'],\n { encoding: 'utf8' },\n );\n return result.status === 0 ? (result.stdout ?? '').trim() : '';\n}\n\n// The one number `n` for a sync and the two things it names: the pre-merge backup branch and its\n// paired conflict-context run dir.\nclass SyncSlot {\n backupBranch: string;\n runDir: string;\n\n constructor(backupBranch: string, runDir: string) {\n this.backupBranch = backupBranch;\n this.runDir = runDir;\n }\n}\n\n// Pick this sync's slot number MONOTONICALLY from the durable audit dirs — one past the highest\n// existing `<home>/merge-<n>/`, then stepped past any existing `<branch>PreMerge<n>` branch so the\n// same `n` is also free for the paired backup branch. Unlike the old first-free-branch scheme, the\n// number is never recycled onto a prior merge's context, so we DO NOT (and must not) wipe the run\n// dir: earlier `merge-<k>/` dirs are the audit trail we now deliberately keep. Returns the backup\n// branch name + the (fresh, non-existent) run dir path, both keyed on `n`.\nfunction chooseSyncSlot(home: string, currentBranch: string): SyncSlot {\n const branchExists = (name: string): boolean =>\n spawnSync('git', ['show-ref', '--verify', '--quiet', `refs/heads/${name}`]).status === 0;\n let n = nextMergeSlotNumber(home);\n while (branchExists(preMergeBackupName(currentBranch, n))) n += 1;\n return new SyncSlot(preMergeBackupName(currentBranch, n), mergeRunDirFor(home, n));\n}\n\n// Snapshot the pre-merge state onto the caller-chosen `backupBranch` (`<currentBranch>PreMerge<n>`),\n// never overwriting. The slot number `n` is picked once in mergeStart and shared with the paired\n// `merge-<n>/` context dir. A clean sync deletes this snapshot at finalize; only conflict syncs keep it.\nfunction createBackup(currentBranch: string, backupBranch: string): void {\n process.stdout.write('\\n' + SEP + '💾 Creating Pre-Merge Backup\\n' + SEP + '\\n');\n runGitChecked(['checkout', '-b', backupBranch], 'Failed to create backup branch');\n runGitChecked(['checkout', currentBranch], 'Failed to return to feature branch');\n process.stdout.write(`✅ Backup created: ${backupBranch}\\n\\n`);\n}\n\nfunction saveConflictContext(\n conflictedFiles: string[], mergeDir: string, forkPoint: string, featureHead: string, mainHead: string,\n): void {\n for (const file of conflictedFiles) {\n const fileDir = perFileContextDir(mergeDir, file);\n fs.mkdirSync(fileDir, { recursive: true });\n\n const fork = spawnSync('git', ['show', `${forkPoint}:${file}`], { encoding: 'utf8' });\n fs.writeFileSync(path.join(fileDir, 'A-forkpoint.txt'), fork.status === 0 ? (fork.stdout ?? '') : '(file did not exist)\\n');\n const feature = spawnSync('git', ['show', `${featureHead}:${file}`], { encoding: 'utf8' });\n fs.writeFileSync(path.join(fileDir, 'B-feature.txt'), feature.status === 0 ? (feature.stdout ?? '') : '(file did not exist)\\n');\n const main = spawnSync('git', ['show', `${mainHead}:${file}`], { encoding: 'utf8' });\n fs.writeFileSync(path.join(fileDir, 'C-main.txt'), main.status === 0 ? (main.stdout ?? '') : '(file did not exist)\\n');\n\n const ba = spawnSync('git', ['diff', forkPoint, featureHead, '--', file], { encoding: 'utf8' });\n fs.writeFileSync(path.join(fileDir, 'B-A.diff'), ba.stdout ?? '');\n const ca = spawnSync('git', ['diff', forkPoint, mainHead, '--', file], { encoding: 'utf8' });\n fs.writeFileSync(path.join(fileDir, 'C-A.diff'), ca.stdout ?? '');\n }\n}\n\n// Single source of truth for the merge process. The script WRITES it at conflict time (rather\n// than the AI relying on a separate hand-maintained doc), parameterized with the live MERGE_DIR\n// and conflicted-file list, so the instructions can never drift from the actual layout. The body\n// is a template constant with {{...}} placeholders so this stays a small filler function.\nconst MERGE_PROCESS_TEMPLATE = `# AI-Assisted Squash-Merge Conflict Resolution (generated)\n\nThis file was generated by \\`pnpm wp-start-update\\` when the 3-point squash-merge hit conflicts.\nIt is the single source of truth for the merge process — follow it exactly.\n\nYou are on branch \\`{{SQUASH_BRANCH}}\\` with conflict markers in the working tree.\n\\`MERGE_DIR = {{MERGE_DIR}}\\`\n\n## How the gate works\n\n- Resolve every conflicted file in the working tree.\n- Run **\\`pnpm {{FINISH_COMMAND}}\\`** — the validation + finish gate. It scans for leftover conflict\n markers, checks each conflicted file has a written merge explanation, validates, and commits the\n merge. (In the PR flow this command is \\`wp-finish-upsert-pr\\`, which additionally runs the\n \\`nx affected\\` build, renders the dashboard, and creates/updates the PR.)\n- **Do NOT run \\`git add\\` / \\`git commit\\` / \\`git push\\` / \\`gh pr\\` yourself.** They are blocked by\n the \\`merge-in-progress-guard\\` hook until the gate validates. The gate does the commit.\n\n## STEP 1 — Load the merge context\n\nPer conflicted file, \\`MERGE_DIR/updatemain-<safe_path>/\\` holds (\\`<safe_path>\\` = path with \\`/\\`→\\`__\\`):\n\n\\`\\`\\`\nA-forkpoint.txt # file at fork point (base)\nB-feature.txt # file on your feature branch\nC-main.txt # file on main\nB-A.diff # what your feature changed (B−A)\nC-A.diff # what main changed (C−A)\n\\`\\`\\`\n\n\\`updatemain-hashes.json\\` holds A/B/C commit hashes. To see why main changed:\n\\`git log <A>..<C> --oneline\\`.\n\n## STEP 2 — Resolve each conflicted file\n\nFor each file: read the working-tree file (the markers) and its \\`B-A.diff\\` / \\`C-A.diff\\` (intent),\nthen Edit to the resolved version, removing ALL conflict markers.\n\nStrategies: goals align & non-overlapping → merge both · one side removes what the other modifies\n→ prefer the removal · same lines, simple (imports/format) → merge both · same lines, complex or\nconflicting goals → ask the user · feature re-implements what main already squashed → prefer\nmain's, then re-apply only the genuinely new feature work.\n\n**Then write a merge explanation** for each conflicted file — NOT a comment in the source (that\nbreaks for JSON and deleted files). Write it next to that file's diffs, at:\n\n\\`\\`\\`\nMERGE_DIR/updatemain-<safe_path>/{{EXPLANATION_FILE}}\n\\`\\`\\`\n\n(\\`<safe_path>\\` = the conflict file path with \\`/\\` → \\`__\\`, the same dir that holds its\n\\`A-forkpoint.txt\\` / \\`B-A.diff\\` / \\`C-A.diff\\`.) In it, explain in a few sentences how you resolved\nthis file: which side you took where, what you combined from B-A.diff vs C-A.diff, and why. The\ngate fails if any conflicted file's explanation is missing or empty. Do not paste A/B/C context\nblocks into the source code.\n\n## STEP 3 — Run the gate (validates the merge AND finalizes it)\n\n\\`\\`\\`\npnpm {{FINISH_COMMAND}}\n\\`\\`\\`\n\n- Leftover conflict markers → fix those files and re-run.\n- Missing merge explanation → write it (see STEP 2) and re-run.\n- Build failure → fix the TypeScript/lint errors and re-run (the gate re-stages for you).\n- Missing review.json (PR flow only) → write it in the printed format (your PR review), then re-run.\n- On success it commits and finalizes the merge (in the PR flow it also renders the dashboard and\n creates/updates the PR).\n\n## Conflicted files\n\n{{FILE_LIST}}\n\n## If you need to bail out\n\nA numbered backup branch was created (e.g. \\`<feature>PreMerge1\\`). To abandon:\n\n\\`\\`\\`\ngit merge --abort 2>/dev/null; git checkout <feature> ; git branch -D {{SQUASH_BRANCH}}\n\\`\\`\\`\n\nThen delete \\`{{MERGE_DIR}}/\\` for a clean slate.\n`;\n\nfunction mergeProcessDoc(mergeDir: string, squashBranch: string, conflictedFiles: string[], finishCommand: string): string {\n const fileList = conflictedFiles.map((f: string): string => `- \\`${f}\\``).join('\\n');\n return MERGE_PROCESS_TEMPLATE\n .replace(/\\{\\{SQUASH_BRANCH\\}\\}/g, squashBranch)\n .replace(/\\{\\{MERGE_DIR\\}\\}/g, mergeDir)\n .replace(/\\{\\{EXPLANATION_FILE\\}\\}/g, MERGE_EXPLANATION_FILE)\n .replace(/\\{\\{FINISH_COMMAND\\}\\}/g, finishCommand)\n .replace(/\\{\\{FILE_LIST\\}\\}/g, fileList);\n}\n\n// Returns the absolute path of the written doc.\nfunction writeMergeProcessDoc(repoRoot: string, mergeDir: string, squashBranch: string, conflictedFiles: string[], finishCommand: string): string {\n const docDir = path.join(repoRoot, WEBPIECES_TMP_DIR, 'instruct-ai');\n fs.mkdirSync(docDir, { recursive: true });\n const docPath = path.join(docDir, 'webpieces.mergeprocess.md');\n fs.writeFileSync(docPath, mergeProcessDoc(mergeDir, squashBranch, conflictedFiles, finishCommand));\n return docPath;\n}\n\n// The AI-facing \"what just happened / what to do next\" recap on the conflict path. Explicit and\n// numbered so an agent (or human) can't miss where the context lives or which command finishes.\nfunction printConflictHandback(\n docPath: string, mergeDir: string, squashBranch: string, conflictedFiles: string[], finishCommand: string,\n): void {\n process.stdout.write('\\n' + SEP + `⚠️ Conflicts in ${conflictedFiles.length} file(s) — handing control back to you\\n` + SEP + '\\n');\n process.stdout.write('Here is exactly what I did and what you need to do:\\n\\n');\n process.stdout.write('What I did:\\n');\n process.stdout.write(' 1. snapshotted your pre-merge state to a PreMerge branch\\n');\n process.stdout.write(' 2. pulled origin/main and squash-merged your work onto it\\n');\n process.stdout.write(` 3. hit conflicts — you are now on the transient branch ${squashBranch}\\n\\n`);\n process.stdout.write('What you need to do:\\n');\n process.stdout.write(` 1. read the merge process doc: ${docPath}\\n`);\n process.stdout.write(` 2. resolve each conflicted file below (its 3-point A/B/C context + diffs are in\\n`);\n process.stdout.write(` ${mergeDir}/updatemain-<file>/), and write that file's merge-explanation.md\\n`);\n process.stdout.write(` 3. run pnpm ${finishCommand} — it validates, commits, and finalizes (do NOT git add/commit/push yourself)\\n\\n`);\n process.stdout.write('Conflicted files:\\n');\n for (const file of conflictedFiles) process.stdout.write(` - ${file}\\n`);\n process.stdout.write('\\n' + SEP);\n}\n\n// Write the conflict context files + the unvalidated marker + the process doc. Does NOT exit — the\n// caller decides (runUpdateFromMain exits 2 to hand back to the AI).\nfunction handleConflictsHandback(\n repoRoot: string, mergeDir: string, currentBranch: string, squashBranch: string,\n backupBranch: string, prNumber: string, hashes: HashPoints, finishCommand: string,\n): void {\n const raw = execSync('git diff --name-only --diff-filter=U', { encoding: 'utf8' }).trim();\n const conflictedFiles = raw.split('\\n').filter((f: string): boolean => f.trim() !== '');\n fs.mkdirSync(mergeDir, { recursive: true }); // run dir was wiped at sync start — create it fresh\n fs.writeFileSync(path.join(mergeDir, 'updatemain-conflicted-files.txt'), raw + '\\n');\n // Copy the A/B/C hashes into THIS run dir so the merge-process doc's `MERGE_DIR/updatemain-hashes.json`\n // pointer is accurate (gatherInfo writes the source copy into the feature home before the slot is known).\n fs.writeFileSync(path.join(mergeDir, 'updatemain-hashes.json'), JSON.stringify(hashes, null, 2) + '\\n');\n saveConflictContext(conflictedFiles, mergeDir, hashes.hashForkPoint, hashes.hashFeatureHead, hashes.hashMainHead);\n\n const marker = new MergeMarker(\n currentBranch, squashBranch, backupBranch, prNumber, conflictedFiles,\n hashes.hashForkPoint, hashes.hashFeatureHead, hashes.hashMainHead, false,\n );\n writeMergeMarker(mergeDir, marker);\n const docPath = writeMergeProcessDoc(repoRoot, mergeDir, squashBranch, conflictedFiles, finishCommand);\n printConflictHandback(docPath, mergeDir, squashBranch, conflictedFiles, finishCommand);\n}\n\n// Short sha of a ref (best-effort — '' if it can't resolve), for the branch-mutation log's\n// oldMain→newMain annotation.\nfunction shortSha(ref: string): string {\n const result = spawnSync('git', ['rev-parse', '--short', ref], { encoding: 'utf8' });\n return result.status === 0 ? (result.stdout ?? '').trim() : '';\n}\n\n// Log the CONFLICT phase with the conflicted-file list + the artifact paths a resolver needs — the\n// per-file 3-point context dir and the generated merge-process doc — so the branch-mutation log alone\n// tells the next agent where to look (no reflog/log archaeology).\nfunction logConflict(repoRoot: string, verb: MutationVerb, mergeDir: string): void {\n const raw = spawnSync('git', ['diff', '--name-only', '--diff-filter=U'], { cwd: repoRoot, encoding: 'utf8' });\n const files = (raw.status === 0 ? (raw.stdout ?? '') : '').split('\\n').map((f: string): string => f.trim()).filter((f: string): boolean => f !== '');\n const event = new BranchMutationEvent(verb, 'CONFLICT');\n event.conflict = true;\n event.conflictFiles = files;\n event.artifacts = [\n path.join(mergeDir, 'updatemain-<file>'),\n path.join(repoRoot, WEBPIECES_TMP_DIR, 'instruct-ai', 'webpieces.mergeprocess.md'),\n ];\n logBranchMutation(repoRoot, event);\n}\n\nexport async function mergeStart(repoRoot: string, verb: MutationVerb, home: string, finishCommand: string): Promise<MergeStartResult> {\n const currentBranch = execSync('git branch --show-current', { encoding: 'utf8' }).trim();\n if (currentBranch.endsWith('Squash')) {\n throw new CliExitError(1, `❌ On a leftover ${currentBranch} branch with no merge marker. Clean up: git branch -D ${currentBranch}`);\n }\n\n // One number `n` for this sync drives BOTH the backup branch and its `merge-<n>/` context dir.\n const slot = chooseSyncSlot(home, currentBranch);\n const backupBranch = slot.backupBranch;\n const mergeDir = slot.runDir;\n\n process.stdout.write('\\n' + SEP + '🔄 Squash-Merge Update from Main\\n' + SEP + '\\n');\n // gatherInfo RETURNS (never exits): an already-even-with-main branch is NOT special-cased here —\n // we flow straight on. The squash merge below becomes a no-op that the `nothingStaged` path\n // (further down) handles, so the caller still proceeds to push + build. (Previously gatherInfo\n // process.exit(0)'d on this case, silently killing wp-start-upsert-pr before push/build.)\n const info = await gatherInfo();\n const hashes = info.hashes;\n if (info.alreadyUpToDate) {\n process.stdout.write('ℹ️ Branch already even with main; nothing to merge, continuing to push/build.\\n');\n }\n\n const prNumber = detectPr(baseBranchName(currentBranch));\n process.stdout.write(prNumber ? `Existing PR #${prNumber} will be updated.\\n` : 'No existing PR (one can be created later).\\n');\n\n createBackup(currentBranch, backupBranch);\n const backupEvent = new BranchMutationEvent(verb, 'BACKUP');\n backupEvent.fromBranch = currentBranch;\n backupEvent.toBranch = backupBranch;\n logBranchMutation(repoRoot, backupEvent);\n\n const squashBranch = `${currentBranch}Squash`;\n if (spawnSync('git', ['show-ref', '--verify', '--quiet', `refs/heads/${squashBranch}`]).status === 0) {\n throw new CliExitError(1, `❌ Stale ${squashBranch} from a previous run. Delete it: git branch -D ${squashBranch}`);\n }\n\n // Branch the squash off origin/main directly — worktree-native. Never checks out or mutates local\n // `main`, which fatals inside a linked worktree (`git checkout main` → \"'main' is already checked\n // out at <primary>\"). origin/main was already fetched in gatherInfo() above, and the fork point +\n // all three hash points (A/B/C) are computed purely from origin/main, so the squash merge base is\n // identical to the old checkout-main + pull path — this is a behavior-preserving change on the\n // primary repo and an unblock inside worktrees.\n const originMainSha = shortSha('origin/main');\n runGitChecked(['checkout', '-b', squashBranch, 'origin/main'], 'Failed to create squash branch off origin/main');\n const baseEvent = new BranchMutationEvent(verb, 'PULL');\n baseEvent.newMain = originMainSha;\n logBranchMutation(repoRoot, baseEvent);\n\n process.stdout.write('\\n' + SEP + `🔀 Squash merging ${currentBranch}\\n` + SEP + '\\n');\n const merge = spawnSync('git', ['merge', '--squash', currentBranch], { stdio: 'inherit' });\n if (merge.status !== 0) {\n handleConflictsHandback(repoRoot, mergeDir, currentBranch, squashBranch, backupBranch, prNumber, hashes, finishCommand);\n logConflict(repoRoot, verb, mergeDir);\n return new MergeStartResult('conflict', null, mergeDir);\n }\n logBranchMutation(repoRoot, new BranchMutationEvent(verb, 'SQUASH'));\n\n const nothingStaged = spawnSync('git', ['diff-index', '--quiet', '--cached', 'HEAD', '--']).status === 0;\n if (nothingStaged) {\n process.stdout.write('ℹ️ Already up-to-date with main (nothing to merge).\\n');\n } else {\n runGitChecked(['commit', '-m', `Squash merge of ${currentBranch}`], 'Failed to commit squash merge');\n // A real merge happened with no conflicts — leave a durable slot record so EVERY merge (not\n // just conflicted ones) is accounted for under merge-info/<feature>/merge-<n>/.\n writeCleanMergeMarker(mergeDir, hashes.hashForkPoint, hashes.hashFeatureHead, hashes.hashMainHead);\n }\n return new MergeStartResult('clean', new MergeContext(currentBranch, squashBranch, backupBranch, prNumber), mergeDir);\n}\n"]}
1
+ {"version":3,"file":"merge-start.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/merge-start.ts"],"names":[],"mappings":"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","sourcesContent":["import { execSync, spawnSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport {\n WEBPIECES_TMP_DIR, MERGE_EXPLANATION_FILE, CliExitError,\n MutationVerb, BranchMutationEvent, logBranchMutation,\n} from '@webpieces/rules-config';\nimport { provideSingleton } from '@webpieces/core-context';\nimport { injectable } from 'inversify';\nimport { GatherInfo } from '../git-gatherInfo';\nimport { BranchNaming } from './branch-naming';\nimport { GitExec } from './git-exec';\nimport { MergeState, MergeMarker } from './merge-state';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\ninterface HashPoints {\n hashForkPoint: string;\n hashFeatureHead: string;\n hashMainHead: string;\n}\n\n// The four branch names merge-END needs to finalize a merge (swap squash→feature, push, clean up).\nexport class MergeContext {\n currentBranch: string;\n squashBranch: string;\n backupBranch: string;\n prNumber: string;\n\n constructor(currentBranch: string, squashBranch: string, backupBranch: string, prNumber: string) {\n this.currentBranch = currentBranch;\n this.squashBranch = squashBranch;\n this.backupBranch = backupBranch;\n this.prNumber = prNumber;\n }\n}\n\n// Outcome of merge-start: 'clean' carries the context for merge-END to finalize; 'conflict' means the\n// marker + context files were written and the caller should hand back to the AI (exit 2).\nexport class MergeStartResult {\n status: 'clean' | 'conflict';\n context: MergeContext | null;\n runDir: string; // this sync's numbered `merge-<n>/` dir — passed to merge-END so it reads THIS marker\n\n constructor(status: 'clean' | 'conflict', context: MergeContext | null, runDir: string) {\n this.status = status;\n this.context = context;\n this.runDir = runDir;\n }\n}\n\n// The one number `n` for a sync and the two things it names: the pre-merge backup branch and its\n// paired conflict-context run dir.\nclass SyncSlot {\n backupBranch: string;\n runDir: string;\n\n constructor(backupBranch: string, runDir: string) {\n this.backupBranch = backupBranch;\n this.runDir = runDir;\n }\n}\n\n// Single source of truth for the merge process (written at conflict time, parameterized with the live\n// MERGE_DIR + conflicted-file list so it can never drift from the actual layout).\nconst MERGE_PROCESS_TEMPLATE = `# AI-Assisted Squash-Merge Conflict Resolution (generated)\n\nThis file was generated by \\`pnpm wp-start-update\\` when the 3-point squash-merge hit conflicts.\nIt is the single source of truth for the merge process — follow it exactly.\n\nYou are on branch \\`{{SQUASH_BRANCH}}\\` with conflict markers in the working tree.\n\\`MERGE_DIR = {{MERGE_DIR}}\\`\n\n## How the gate works\n\n- Resolve every conflicted file in the working tree.\n- Run **\\`pnpm {{FINISH_COMMAND}}\\`** — the validation + finish gate. It scans for leftover conflict\n markers, checks each conflicted file has a written merge explanation, validates, and commits the\n merge. (In the PR flow this command is \\`wp-finish-upsert-pr\\`, which additionally runs the\n \\`nx affected\\` build, renders the dashboard, and creates/updates the PR.)\n- **Do NOT run \\`git add\\` / \\`git commit\\` / \\`git push\\` / \\`gh pr\\` yourself.** They are blocked by\n the \\`merge-in-progress-guard\\` hook until the gate validates. The gate does the commit.\n\n## STEP 1 — Load the merge context\n\nPer conflicted file, \\`MERGE_DIR/updatemain-<safe_path>/\\` holds (\\`<safe_path>\\` = path with \\`/\\`→\\`__\\`):\n\n\\`\\`\\`\nA-forkpoint.txt # file at fork point (base)\nB-feature.txt # file on your feature branch\nC-main.txt # file on main\nB-A.diff # what your feature changed (B−A)\nC-A.diff # what main changed (C−A)\n\\`\\`\\`\n\n\\`updatemain-hashes.json\\` holds A/B/C commit hashes. To see why main changed:\n\\`git log <A>..<C> --oneline\\`.\n\n## STEP 2 — Resolve each conflicted file\n\nFor each file: read the working-tree file (the markers) and its \\`B-A.diff\\` / \\`C-A.diff\\` (intent),\nthen Edit to the resolved version, removing ALL conflict markers.\n\nStrategies: goals align & non-overlapping → merge both · one side removes what the other modifies\n→ prefer the removal · same lines, simple (imports/format) → merge both · same lines, complex or\nconflicting goals → ask the user · feature re-implements what main already squashed → prefer\nmain's, then re-apply only the genuinely new feature work.\n\n**Then write a merge explanation** for each conflicted file — NOT a comment in the source (that\nbreaks for JSON and deleted files). Write it next to that file's diffs, at:\n\n\\`\\`\\`\nMERGE_DIR/updatemain-<safe_path>/{{EXPLANATION_FILE}}\n\\`\\`\\`\n\n(\\`<safe_path>\\` = the conflict file path with \\`/\\` → \\`__\\`, the same dir that holds its\n\\`A-forkpoint.txt\\` / \\`B-A.diff\\` / \\`C-A.diff\\`.) In it, explain in a few sentences how you resolved\nthis file: which side you took where, what you combined from B-A.diff vs C-A.diff, and why. The\ngate fails if any conflicted file's explanation is missing or empty. Do not paste A/B/C context\nblocks into the source code.\n\n## STEP 3 — Run the gate (validates the merge AND finalizes it)\n\n\\`\\`\\`\npnpm {{FINISH_COMMAND}}\n\\`\\`\\`\n\n- Leftover conflict markers → fix those files and re-run.\n- Missing merge explanation → write it (see STEP 2) and re-run.\n- Build failure → fix the TypeScript/lint errors and re-run (the gate re-stages for you).\n- Missing review.json (PR flow only) → write it in the printed format (your PR review), then re-run.\n- On success it commits and finalizes the merge (in the PR flow it also renders the dashboard and\n creates/updates the PR).\n\n## Conflicted files\n\n{{FILE_LIST}}\n\n## If you need to bail out\n\nA numbered backup branch was created (e.g. \\`<feature>PreMerge1\\`). To abandon:\n\n\\`\\`\\`\ngit merge --abort 2>/dev/null; git checkout <feature> ; git branch -D {{SQUASH_BRANCH}}\n\\`\\`\\`\n\nThen delete \\`{{MERGE_DIR}}/\\` for a clean slate.\n`;\n\n// merge-START: the first half of the 3-point squash-merge lifecycle. Brings origin/main into a fresh\n// `<branch>Squash`, and on conflict writes the 3-point context + unvalidated marker + process doc,\n// then hands control back to the AI. On a clean merge it commits the squash and returns the branch\n// context so the caller (RunUpdate) can run merge-END to finalize. NEVER finalizes or posts a PR.\n@provideSingleton()\n@injectable()\nexport class MergeStart {\n constructor(\n private readonly gatherInfo: GatherInfo,\n private readonly branchNaming: BranchNaming,\n private readonly gitExec: GitExec,\n private readonly mergeState: MergeState,\n ) {}\n\n async mergeStart(repoRoot: string, verb: MutationVerb, home: string, finishCommand: string): Promise<MergeStartResult> {\n const currentBranch = execSync('git branch --show-current', { encoding: 'utf8' }).trim();\n if (currentBranch.endsWith('Squash')) {\n throw new CliExitError(1, `❌ On a leftover ${currentBranch} branch with no merge marker. Clean up: git branch -D ${currentBranch}`);\n }\n\n // One number `n` for this sync drives BOTH the backup branch and its `merge-<n>/` context dir.\n const slot = this.chooseSyncSlot(home, currentBranch);\n const backupBranch = slot.backupBranch;\n const mergeDir = slot.runDir;\n\n process.stdout.write('\\n' + SEP + '🔄 Squash-Merge Update from Main\\n' + SEP + '\\n');\n const info = await this.gatherInfo.gatherInfo();\n const hashes = info.hashes;\n if (info.alreadyUpToDate) {\n process.stdout.write('ℹ️ Branch already even with main; nothing to merge, continuing to push/build.\\n');\n }\n\n const prNumber = this.detectPr(this.branchNaming.baseBranchName(currentBranch));\n process.stdout.write(prNumber ? `Existing PR #${prNumber} will be updated.\\n` : 'No existing PR (one can be created later).\\n');\n\n this.createBackup(currentBranch, backupBranch);\n const backupEvent = new BranchMutationEvent(verb, 'BACKUP');\n backupEvent.fromBranch = currentBranch;\n backupEvent.toBranch = backupBranch;\n logBranchMutation(repoRoot, backupEvent);\n\n const squashBranch = `${currentBranch}Squash`;\n if (spawnSync('git', ['show-ref', '--verify', '--quiet', `refs/heads/${squashBranch}`]).status === 0) {\n throw new CliExitError(1, `❌ Stale ${squashBranch} from a previous run. Delete it: git branch -D ${squashBranch}`);\n }\n\n // Branch the squash off origin/main directly — worktree-native. origin/main was already fetched\n // in gatherInfo(), and A/B/C are computed purely from origin/main, so the merge base is identical\n // to the old checkout-main + pull path.\n const originMainSha = this.shortSha('origin/main');\n this.gitExec.runGitChecked(['checkout', '-b', squashBranch, 'origin/main'], 'Failed to create squash branch off origin/main');\n const baseEvent = new BranchMutationEvent(verb, 'PULL');\n baseEvent.newMain = originMainSha;\n logBranchMutation(repoRoot, baseEvent);\n\n process.stdout.write('\\n' + SEP + `🔀 Squash merging ${currentBranch}\\n` + SEP + '\\n');\n const merge = spawnSync('git', ['merge', '--squash', currentBranch], { stdio: 'inherit' });\n if (merge.status !== 0) {\n this.handleConflictsHandback(repoRoot, mergeDir, currentBranch, squashBranch, backupBranch, prNumber, hashes, finishCommand);\n this.logConflict(repoRoot, verb, mergeDir);\n return new MergeStartResult('conflict', null, mergeDir);\n }\n logBranchMutation(repoRoot, new BranchMutationEvent(verb, 'SQUASH'));\n\n const nothingStaged = spawnSync('git', ['diff-index', '--quiet', '--cached', 'HEAD', '--']).status === 0;\n if (nothingStaged) {\n process.stdout.write('ℹ️ Already up-to-date with main (nothing to merge).\\n');\n } else {\n this.gitExec.runGitChecked(['commit', '-m', `Squash merge of ${currentBranch}`], 'Failed to commit squash merge');\n this.mergeState.writeCleanMergeMarker(mergeDir, hashes.hashForkPoint, hashes.hashFeatureHead, hashes.hashMainHead);\n }\n return new MergeStartResult('clean', new MergeContext(currentBranch, squashBranch, backupBranch, prNumber), mergeDir);\n }\n\n // Detect the PR by its STABLE feature branch (a leftover `…wpN` still resolves to the one name the\n // PR lives on).\n private detectPr(baseBranch: string): string {\n const result = spawnSync(\n 'gh', ['pr', 'list', '--head', baseBranch, '--json', 'number', '--jq', '.[0].number'],\n { encoding: 'utf8' },\n );\n return result.status === 0 ? (result.stdout ?? '').trim() : '';\n }\n\n // Pick this sync's slot number MONOTONICALLY from the durable audit dirs, then step past any\n // existing `<branch>PreMerge<n>` branch so the same `n` is free for the paired backup branch.\n private chooseSyncSlot(home: string, currentBranch: string): SyncSlot {\n const branchExists = (name: string): boolean =>\n spawnSync('git', ['show-ref', '--verify', '--quiet', `refs/heads/${name}`]).status === 0;\n let n = this.mergeState.nextMergeSlotNumber(home);\n while (branchExists(this.branchNaming.preMergeBackupName(currentBranch, n))) n += 1;\n return new SyncSlot(this.branchNaming.preMergeBackupName(currentBranch, n), this.mergeState.mergeRunDirFor(home, n));\n }\n\n // Snapshot the pre-merge state onto the caller-chosen `backupBranch`, never overwriting.\n private createBackup(currentBranch: string, backupBranch: string): void {\n process.stdout.write('\\n' + SEP + '💾 Creating Pre-Merge Backup\\n' + SEP + '\\n');\n this.gitExec.runGitChecked(['checkout', '-b', backupBranch], 'Failed to create backup branch');\n this.gitExec.runGitChecked(['checkout', currentBranch], 'Failed to return to feature branch');\n process.stdout.write(`✅ Backup created: ${backupBranch}\\n\\n`);\n }\n\n private saveConflictContext(\n conflictedFiles: string[], mergeDir: string, forkPoint: string, featureHead: string, mainHead: string,\n ): void {\n for (const file of conflictedFiles) {\n const fileDir = this.mergeState.perFileContextDir(mergeDir, file);\n fs.mkdirSync(fileDir, { recursive: true });\n\n const fork = spawnSync('git', ['show', `${forkPoint}:${file}`], { encoding: 'utf8' });\n fs.writeFileSync(path.join(fileDir, 'A-forkpoint.txt'), fork.status === 0 ? (fork.stdout ?? '') : '(file did not exist)\\n');\n const feature = spawnSync('git', ['show', `${featureHead}:${file}`], { encoding: 'utf8' });\n fs.writeFileSync(path.join(fileDir, 'B-feature.txt'), feature.status === 0 ? (feature.stdout ?? '') : '(file did not exist)\\n');\n const main = spawnSync('git', ['show', `${mainHead}:${file}`], { encoding: 'utf8' });\n fs.writeFileSync(path.join(fileDir, 'C-main.txt'), main.status === 0 ? (main.stdout ?? '') : '(file did not exist)\\n');\n\n const ba = spawnSync('git', ['diff', forkPoint, featureHead, '--', file], { encoding: 'utf8' });\n fs.writeFileSync(path.join(fileDir, 'B-A.diff'), ba.stdout ?? '');\n const ca = spawnSync('git', ['diff', forkPoint, mainHead, '--', file], { encoding: 'utf8' });\n fs.writeFileSync(path.join(fileDir, 'C-A.diff'), ca.stdout ?? '');\n }\n }\n\n private mergeProcessDoc(mergeDir: string, squashBranch: string, conflictedFiles: string[], finishCommand: string): string {\n const fileList = conflictedFiles.map((f: string): string => `- \\`${f}\\``).join('\\n');\n return MERGE_PROCESS_TEMPLATE\n .replace(/\\{\\{SQUASH_BRANCH\\}\\}/g, squashBranch)\n .replace(/\\{\\{MERGE_DIR\\}\\}/g, mergeDir)\n .replace(/\\{\\{EXPLANATION_FILE\\}\\}/g, MERGE_EXPLANATION_FILE)\n .replace(/\\{\\{FINISH_COMMAND\\}\\}/g, finishCommand)\n .replace(/\\{\\{FILE_LIST\\}\\}/g, fileList);\n }\n\n // Returns the absolute path of the written doc.\n private writeMergeProcessDoc(repoRoot: string, mergeDir: string, squashBranch: string, conflictedFiles: string[], finishCommand: string): string {\n const docDir = path.join(repoRoot, WEBPIECES_TMP_DIR, 'instruct-ai');\n fs.mkdirSync(docDir, { recursive: true });\n const docPath = path.join(docDir, 'webpieces.mergeprocess.md');\n fs.writeFileSync(docPath, this.mergeProcessDoc(mergeDir, squashBranch, conflictedFiles, finishCommand));\n return docPath;\n }\n\n // The AI-facing \"what just happened / what to do next\" recap on the conflict path.\n private printConflictHandback(\n docPath: string, mergeDir: string, squashBranch: string, conflictedFiles: string[], finishCommand: string,\n ): void {\n process.stdout.write('\\n' + SEP + `⚠️ Conflicts in ${conflictedFiles.length} file(s) — handing control back to you\\n` + SEP + '\\n');\n process.stdout.write('Here is exactly what I did and what you need to do:\\n\\n');\n process.stdout.write('What I did:\\n');\n process.stdout.write(' 1. snapshotted your pre-merge state to a PreMerge branch\\n');\n process.stdout.write(' 2. pulled origin/main and squash-merged your work onto it\\n');\n process.stdout.write(` 3. hit conflicts — you are now on the transient branch ${squashBranch}\\n\\n`);\n process.stdout.write('What you need to do:\\n');\n process.stdout.write(` 1. read the merge process doc: ${docPath}\\n`);\n process.stdout.write(` 2. resolve each conflicted file below (its 3-point A/B/C context + diffs are in\\n`);\n process.stdout.write(` ${mergeDir}/updatemain-<file>/), and write that file's merge-explanation.md\\n`);\n process.stdout.write(` 3. run pnpm ${finishCommand} — it validates, commits, and finalizes (do NOT git add/commit/push yourself)\\n\\n`);\n process.stdout.write('Conflicted files:\\n');\n for (const file of conflictedFiles) process.stdout.write(` - ${file}\\n`);\n process.stdout.write('\\n' + SEP);\n }\n\n // Write the conflict context files + the unvalidated marker + the process doc. Does NOT exit.\n private handleConflictsHandback(\n repoRoot: string, mergeDir: string, currentBranch: string, squashBranch: string,\n backupBranch: string, prNumber: string, hashes: HashPoints, finishCommand: string,\n ): void {\n const raw = execSync('git diff --name-only --diff-filter=U', { encoding: 'utf8' }).trim();\n const conflictedFiles = raw.split('\\n').filter((f: string): boolean => f.trim() !== '');\n fs.mkdirSync(mergeDir, { recursive: true });\n fs.writeFileSync(path.join(mergeDir, 'updatemain-conflicted-files.txt'), raw + '\\n');\n fs.writeFileSync(path.join(mergeDir, 'updatemain-hashes.json'), JSON.stringify(hashes, null, 2) + '\\n');\n this.saveConflictContext(conflictedFiles, mergeDir, hashes.hashForkPoint, hashes.hashFeatureHead, hashes.hashMainHead);\n\n const marker = new MergeMarker(\n currentBranch, squashBranch, backupBranch, prNumber, conflictedFiles,\n hashes.hashForkPoint, hashes.hashFeatureHead, hashes.hashMainHead, false,\n );\n this.mergeState.writeMergeMarker(mergeDir, marker);\n const docPath = this.writeMergeProcessDoc(repoRoot, mergeDir, squashBranch, conflictedFiles, finishCommand);\n this.printConflictHandback(docPath, mergeDir, squashBranch, conflictedFiles, finishCommand);\n }\n\n // Short sha of a ref (best-effort — '' if it can't resolve).\n private shortSha(ref: string): string {\n const result = spawnSync('git', ['rev-parse', '--short', ref], { encoding: 'utf8' });\n return result.status === 0 ? (result.stdout ?? '').trim() : '';\n }\n\n // Log the CONFLICT phase with the conflicted-file list + artifact paths a resolver needs.\n private logConflict(repoRoot: string, verb: MutationVerb, mergeDir: string): void {\n const raw = spawnSync('git', ['diff', '--name-only', '--diff-filter=U'], { cwd: repoRoot, encoding: 'utf8' });\n const files = (raw.status === 0 ? (raw.stdout ?? '') : '').split('\\n').map((f: string): string => f.trim()).filter((f: string): boolean => f !== '');\n const event = new BranchMutationEvent(verb, 'CONFLICT');\n event.conflict = true;\n event.conflictFiles = files;\n event.artifacts = [\n path.join(mergeDir, 'updatemain-<file>'),\n path.join(repoRoot, WEBPIECES_TMP_DIR, 'instruct-ai', 'webpieces.mergeprocess.md'),\n ];\n logBranchMutation(repoRoot, event);\n }\n}\n"]}