@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,20 +1,29 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getFeatureName = getFeatureName;
4
- exports.main = main;
3
+ exports.AiBranchName = void 0;
4
+ const tslib_1 = require("tslib");
5
5
  const child_process_1 = require("child_process");
6
- const rules_config_1 = require("@webpieces/rules-config");
6
+ const core_context_1 = require("@webpieces/core-context");
7
+ const inversify_1 = require("inversify");
7
8
  const branch_naming_1 = require("./branch-naming");
8
- // Stable feature identity used to key the merge-context dir and PR-body dir. It MUST stay constant
9
- // across a sync's transient `<feature>Squash` temp branch (and any leftover `…wpN` from the old
10
- // scheme), so derive it from baseBranchName (strips `Squash` + a legacy `wpN`) before slugifying.
11
- function getFeatureName() {
12
- const branch = (0, child_process_1.execSync)('git branch --show-current', { encoding: 'utf8' }).trim();
13
- return (0, branch_naming_1.baseBranchName)(branch).replace(/\//g, '-');
14
- }
15
- async function main() {
16
- process.stdout.write(getFeatureName() + '\n');
17
- }
18
- if (require.main === module)
19
- (0, rules_config_1.runMain)(main);
9
+ /** The stable feature identity used to key the merge-context + PR-body dirs. */
10
+ let AiBranchName = class AiBranchName {
11
+ branchNaming;
12
+ constructor(branchNaming) {
13
+ this.branchNaming = branchNaming;
14
+ }
15
+ // Stable feature identity used to key the merge-context dir and PR-body dir. It MUST stay constant
16
+ // across a sync's transient `<feature>Squash` temp branch (and any leftover `…wpN` from the old
17
+ // scheme), so derive it from baseBranchName (strips `Squash` + a legacy `wpN`) before slugifying.
18
+ getFeatureName() {
19
+ const branch = (0, child_process_1.execSync)('git branch --show-current', { encoding: 'utf8' }).trim();
20
+ return this.branchNaming.baseBranchName(branch).replace(/\//g, '-');
21
+ }
22
+ };
23
+ exports.AiBranchName = AiBranchName;
24
+ exports.AiBranchName = AiBranchName = tslib_1.__decorate([
25
+ (0, core_context_1.provideSingleton)(),
26
+ (0, inversify_1.injectable)(),
27
+ tslib_1.__metadata("design:paramtypes", [branch_naming_1.BranchNaming])
28
+ ], AiBranchName);
20
29
  //# sourceMappingURL=git-readAiBranchName.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"git-readAiBranchName.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/git-readAiBranchName.ts"],"names":[],"mappings":";;AAOA,wCAGC;AAED,oBAEC;AAdD,iDAAyC;AACzC,0DAAkD;AAClD,mDAAiD;AAEjD,mGAAmG;AACnG,gGAAgG;AAChG,kGAAkG;AAClG,SAAgB,cAAc;IAC1B,MAAM,MAAM,GAAG,IAAA,wBAAQ,EAAC,2BAA2B,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IAClF,OAAO,IAAA,8BAAc,EAAC,MAAM,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;AACtD,CAAC;AAEM,KAAK,UAAU,IAAI;IACtB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,cAAc,EAAE,GAAG,IAAI,CAAC,CAAC;AAClD,CAAC;AAED,IAAI,OAAO,CAAC,IAAI,KAAK,MAAM;IAAE,IAAA,sBAAO,EAAC,IAAI,CAAC,CAAC","sourcesContent":["import { execSync } from 'child_process';\nimport { runMain } from '@webpieces/rules-config';\nimport { baseBranchName } from './branch-naming';\n\n// Stable feature identity used to key the merge-context dir and PR-body dir. It MUST stay constant\n// across a sync's transient `<feature>Squash` temp branch (and any leftover `…wpN` from the old\n// scheme), so derive it from baseBranchName (strips `Squash` + a legacy `wpN`) before slugifying.\nexport function getFeatureName(): string {\n const branch = execSync('git branch --show-current', { encoding: 'utf8' }).trim();\n return baseBranchName(branch).replace(/\\//g, '-');\n}\n\nexport async function main(): Promise<void> {\n process.stdout.write(getFeatureName() + '\\n');\n}\n\nif (require.main === module) runMain(main);\n"]}
1
+ {"version":3,"file":"git-readAiBranchName.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/git-readAiBranchName.ts"],"names":[],"mappings":";;;;AAAA,iDAAyC;AACzC,0DAA2D;AAC3D,yCAAuC;AACvC,mDAA+C;AAE/C,gFAAgF;AAGzE,IAAM,YAAY,GAAlB,MAAM,YAAY;IACQ;IAA7B,YAA6B,YAA0B;QAA1B,iBAAY,GAAZ,YAAY,CAAc;IAAG,CAAC;IAE3D,mGAAmG;IACnG,gGAAgG;IAChG,kGAAkG;IAClG,cAAc;QACV,MAAM,MAAM,GAAG,IAAA,wBAAQ,EAAC,2BAA2B,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QAClF,OAAO,IAAI,CAAC,YAAY,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IACxE,CAAC;CACJ,CAAA;AAVY,oCAAY;uBAAZ,YAAY;IAFxB,IAAA,+BAAgB,GAAE;IAClB,IAAA,sBAAU,GAAE;6CAEkC,4BAAY;GAD9C,YAAY,CAUxB","sourcesContent":["import { execSync } from 'child_process';\nimport { provideSingleton } from '@webpieces/core-context';\nimport { injectable } from 'inversify';\nimport { BranchNaming } from './branch-naming';\n\n/** The stable feature identity used to key the merge-context + PR-body dirs. */\n@provideSingleton()\n@injectable()\nexport class AiBranchName {\n constructor(private readonly branchNaming: BranchNaming) {}\n\n // Stable feature identity used to key the merge-context dir and PR-body dir. It MUST stay constant\n // across a sync's transient `<feature>Squash` temp branch (and any leftover `…wpN` from the old\n // scheme), so derive it from baseBranchName (strips `Squash` + a legacy `wpN`) before slugifying.\n getFeatureName(): string {\n const branch = execSync('git branch --show-current', { encoding: 'utf8' }).trim();\n return this.branchNaming.baseBranchName(branch).replace(/\\//g, '-');\n }\n}\n"]}
@@ -1,8 +1,23 @@
1
1
  import { MutationVerb } from '@webpieces/rules-config';
2
+ import { BranchNaming } from './branch-naming';
3
+ import { CleanTmp } from './cleanTmp';
4
+ import { GitExec } from './git-exec';
2
5
  import { MergeContext } from './merge-start';
3
- /**
4
- * Complete a 3-point squash merge. `conflictedFiles` non-null means a conflict was resolved by the AI
5
- * and must be validated + committed before finalizing; null means a clean merge that merge-START
6
- * already committed (finalize only). Either way the merge ends fully finalized on the feature branch.
7
- */
8
- export declare function mergeEnd(repoRoot: string, verb: MutationVerb, mergeDir: string, ctx: MergeContext, conflictedFiles: string[] | null): Promise<void>;
6
+ import { MergeState } from './merge-state';
7
+ export declare class MergeEnd {
8
+ private readonly branchNaming;
9
+ private readonly cleanTmpService;
10
+ private readonly gitExec;
11
+ private readonly mergeState;
12
+ constructor(branchNaming: BranchNaming, cleanTmpService: CleanTmp, gitExec: GitExec, mergeState: MergeState);
13
+ /**
14
+ * Complete a 3-point squash merge. `conflictedFiles` non-null means a conflict was resolved by the
15
+ * AI and must be validated + committed before finalizing; null means a clean merge that merge-START
16
+ * already committed (finalize only). Either way the merge ends fully finalized on the feature branch.
17
+ */
18
+ mergeEnd(repoRoot: string, verb: MutationVerb, mergeDir: string, ctx: MergeContext, conflictedFiles: string[] | null): Promise<void>;
19
+ private validateResolution;
20
+ private localBranchExists;
21
+ private finalizeBranch;
22
+ private printSyncRecap;
23
+ }
@@ -1,153 +1,167 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.mergeEnd = mergeEnd;
3
+ exports.MergeEnd = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const child_process_1 = require("child_process");
6
6
  const fs = tslib_1.__importStar(require("fs"));
7
7
  const path = tslib_1.__importStar(require("path"));
8
8
  const rules_config_1 = require("@webpieces/rules-config");
9
+ const core_context_1 = require("@webpieces/core-context");
10
+ const inversify_1 = require("inversify");
9
11
  const branch_naming_1 = require("./branch-naming");
10
12
  const cleanTmp_1 = require("./cleanTmp");
11
13
  const git_exec_1 = require("./git-exec");
12
14
  const merge_state_1 = require("./merge-state");
15
+ const SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
13
16
  // merge-END: the second half of the 3-point squash-merge lifecycle, symmetric with merge-START. Given
14
17
  // the branch context, it (optionally) validates + commits the AI's conflict resolution, then ALWAYS
15
- // finalizes the merge — force-pushes `<branch>Squash` to the stable feature branch and renames it
16
- // BACK to that same feature name (local == remote == PR head), stamps a clean main-sync status,
17
- // clears the marker and sweeps stale tmp. The shared runUpdateFromMain (clean path / validated resume), wp-finish-update, and
18
- // wp-finish-upsert-pr (conflict resolution) all call THIS, so finalization happens in exactly one
19
- // place and the conflict path can no longer post a PR from the un-swapped squash branch.
20
- const SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
21
- // Validate the AI's resolution of the conflicted files — the part of the process the AI owns
22
- // (branch creation/finalization is the script's job, so it is not re-checked here). Throws
23
- // CliExitError with a fix instruction on any failure; returns only when all three checks pass.
24
- function validateResolution(repoRoot, mergeDir, conflictedFiles) {
25
- // 1. Scoped conflict-marker scan (only the conflicted files — O(conflicts), not O(repo)).
26
- const scan = (0, merge_state_1.scanConflictMarkers)(repoRoot, conflictedFiles);
27
- if (!scan.clean) {
28
- throw new rules_config_1.CliExitError(1, '❌ Unresolved conflict markers (<<<<<<< / ======= / >>>>>>>) remain in:\n' +
29
- scan.filesWithMarkers.map((file) => ` - ${file}`).join('\n') +
30
- '\n\nResolve them, then re-run: pnpm wp-finish-upsert-pr');
31
- }
32
- // 2. Ensure git itself has no remaining unmerged entries.
33
- const unmerged = (0, child_process_1.execSync)('git diff --name-only --diff-filter=U', { encoding: 'utf8' }).trim();
34
- if (unmerged !== '') {
35
- throw new rules_config_1.CliExitError(1, '❌ Git still reports unmerged files:\n' + unmerged +
36
- '\n\nResolve and `git add` them, then re-run: pnpm wp-finish-upsert-pr');
37
- }
38
- process.stdout.write('✅ No conflict markers in resolved files.\n');
39
- // 3. Explanation check — every conflicted file must have a non-empty merge-explanation.md in
40
- // its per-file context dir, proving the AI deliberately 3-point merged it (and recording how)
41
- // rather than blindly taking one side. A sidecar file works for any type, incl. JSON/deletes.
42
- const explanations = (0, merge_state_1.scanMergeExplanations)(mergeDir, conflictedFiles);
43
- if (!explanations.clean) {
44
- const missing = explanations.filesWithMarkers
45
- .map((file) => ` - ${file}\n → ${path.join((0, merge_state_1.perFileContextDir)(mergeDir, file), rules_config_1.MERGE_EXPLANATION_FILE)}`)
46
- .join('\n');
47
- throw new rules_config_1.CliExitError(1, `❌ Missing/empty merge explanation (${rules_config_1.MERGE_EXPLANATION_FILE}) for:\n` +
48
- missing +
49
- '\n\nWrite a few sentences on how you resolved each (which side, what you combined, why),\n' +
50
- 'then re-run: pnpm wp-finish-upsert-pr');
18
+ // finalizes — force-pushes `<branch>Squash` to the stable feature branch and renames it BACK to that
19
+ // same feature name (local == remote == PR head), stamps a clean main-sync status, clears the marker,
20
+ // and sweeps stale tmp. RunUpdate (clean path / validated resume), wp-finish-update, and
21
+ // wp-finish-upsert-pr (conflict resolution) all call THIS, so finalization happens in exactly one place.
22
+ let MergeEnd = class MergeEnd {
23
+ branchNaming;
24
+ cleanTmpService;
25
+ gitExec;
26
+ mergeState;
27
+ constructor(branchNaming, cleanTmpService, gitExec, mergeState) {
28
+ this.branchNaming = branchNaming;
29
+ this.cleanTmpService = cleanTmpService;
30
+ this.gitExec = gitExec;
31
+ this.mergeState = mergeState;
51
32
  }
52
- process.stdout.write('✅ Merge explanations present for all resolved files.\n');
53
- }
54
- function localBranchExists(name) {
55
- return (0, child_process_1.spawnSync)('git', ['show-ref', '--verify', '--quiet', `refs/heads/${name}`]).status === 0;
56
- }
57
- // Force-push the squash branch to the stable feature branch (where the single PR lives), then RENAME
58
- // the local squash branch back to that SAME feature name — so the local branch, the remote branch,
59
- // and the PR head are all one name (no `wpN` divergence). On a CLEAN sync (`!hadConflict`) the
60
- // pre-merge snapshot is disposable, so it's deleted at the very end; a CONFLICT sync keeps it (paired
61
- // with its `merge-<n>/` context). Ends with an explicit "here is exactly what I did" recap.
62
- function finalizeBranch(repoRoot, verb, ctx, hadConflict) {
63
- process.stdout.write('\n' + SEP + '🗑️ Finalizing\n' + SEP + '\n');
64
- const base = (0, branch_naming_1.baseBranchName)(ctx.currentBranch);
65
- (0, git_exec_1.runGitChecked)(['branch', '-D', ctx.currentBranch], 'Failed to delete old feature branch');
66
- const remoteExists = (0, child_process_1.spawnSync)('git', ['ls-remote', '--exit-code', '--heads', 'origin', base]).status === 0;
67
- if (remoteExists) {
68
- process.stdout.write(ctx.prNumber ? `Updating PR #${ctx.prNumber} (force-with-lease)...\n` : 'Updating remote branch (force-with-lease)...\n');
69
- (0, git_exec_1.runGitChecked)(['push', '-u', '--force-with-lease', 'origin', `${ctx.squashBranch}:${base}`], 'Failed to push to origin');
70
- }
71
- else {
72
- process.stdout.write('No remote branch — local only.\n');
33
+ /**
34
+ * Complete a 3-point squash merge. `conflictedFiles` non-null means a conflict was resolved by the
35
+ * AI and must be validated + committed before finalizing; null means a clean merge that merge-START
36
+ * already committed (finalize only). Either way the merge ends fully finalized on the feature branch.
37
+ */
38
+ async mergeEnd(repoRoot, verb, mergeDir, ctx, conflictedFiles) {
39
+ if (conflictedFiles !== null) {
40
+ process.stdout.write('\n' + SEP + '🔎 Validating Merge Resolution\n' + SEP + '\n');
41
+ this.validateResolution(repoRoot, mergeDir, conflictedFiles);
42
+ // Stage the AI's resolved conflicts, but NEVER sweep untracked files into the squash commit.
43
+ // Fail on untracked so the AI commits or deletes them explicitly; then `git add -u` stages
44
+ // tracked resolutions only.
45
+ this.gitExec.assertNoUntracked(repoRoot);
46
+ this.gitExec.runGitChecked(['add', '-u'], 'Failed to stage resolved files');
47
+ const nothingStaged = (0, child_process_1.spawnSync)('git', ['diff-index', '--quiet', '--cached', 'HEAD', '--']).status === 0;
48
+ if (!nothingStaged) {
49
+ this.gitExec.runGitChecked(['commit', '-m', `Squash merge of ${ctx.currentBranch} (conflicts resolved)`], 'Failed to commit resolved merge');
50
+ }
51
+ fs.writeFileSync(path.join(mergeDir, 'conflicts-resolved'), '');
52
+ process.stdout.write('\n✅ Merge validated and committed.\n');
53
+ }
54
+ // A marker in THIS run dir means the sync hit conflicts (marker is written only on hand-back).
55
+ // Read it BEFORE clearMergeMarker so finalize knows whether to keep the pre-merge snapshot.
56
+ const hadConflict = this.mergeState.readMergeMarker(mergeDir) !== null;
57
+ this.finalizeBranch(repoRoot, verb, ctx, hadConflict);
58
+ this.mergeState.clearMergeMarker(mergeDir);
59
+ await this.cleanTmpService.cleanTmp();
73
60
  }
74
- (0, git_exec_1.runGitChecked)(['checkout', ctx.squashBranch], 'Failed to checkout squash branch');
75
- // Free the rename target: `base` is normally the branch we just deleted (ctx.currentBranch), but on
76
- // a backward-compat sync from a leftover `…wpN` a separate stale `base` can linger — drop it too.
77
- if (base !== ctx.currentBranch && localBranchExists(base)) {
78
- (0, git_exec_1.runGitChecked)(['branch', '-D', base], 'Failed to delete stale base branch');
61
+ // Validate the AI's resolution of the conflicted files. Throws CliExitError with a fix instruction
62
+ // on any failure; returns only when all three checks pass.
63
+ validateResolution(repoRoot, mergeDir, conflictedFiles) {
64
+ // 1. Scoped conflict-marker scan (only the conflicted files — O(conflicts), not O(repo)).
65
+ const scan = this.mergeState.scanConflictMarkers(repoRoot, conflictedFiles);
66
+ if (!scan.clean) {
67
+ throw new rules_config_1.CliExitError(1, '❌ Unresolved conflict markers (<<<<<<< / ======= / >>>>>>>) remain in:\n' +
68
+ scan.filesWithMarkers.map((file) => ` - ${file}`).join('\n') +
69
+ '\n\nResolve them, then re-run: pnpm wp-finish-upsert-pr');
70
+ }
71
+ // 2. Ensure git itself has no remaining unmerged entries.
72
+ const unmerged = (0, child_process_1.execSync)('git diff --name-only --diff-filter=U', { encoding: 'utf8' }).trim();
73
+ if (unmerged !== '') {
74
+ throw new rules_config_1.CliExitError(1, '❌ Git still reports unmerged files:\n' + unmerged +
75
+ '\n\nResolve and `git add` them, then re-run: pnpm wp-finish-upsert-pr');
76
+ }
77
+ process.stdout.write('✅ No conflict markers in resolved files.\n');
78
+ // 3. Explanation check — every conflicted file must have a non-empty merge-explanation.md in its
79
+ // per-file context dir, proving the AI deliberately 3-point merged it (and recording how).
80
+ const explanations = this.mergeState.scanMergeExplanations(mergeDir, conflictedFiles);
81
+ if (!explanations.clean) {
82
+ const missing = explanations.filesWithMarkers
83
+ .map((file) => ` - ${file}\n → ${path.join(this.mergeState.perFileContextDir(mergeDir, file), rules_config_1.MERGE_EXPLANATION_FILE)}`)
84
+ .join('\n');
85
+ throw new rules_config_1.CliExitError(1, `❌ Missing/empty merge explanation (${rules_config_1.MERGE_EXPLANATION_FILE}) for:\n` +
86
+ missing +
87
+ '\n\nWrite a few sentences on how you resolved each (which side, what you combined, why),\n' +
88
+ 'then re-run: pnpm wp-finish-upsert-pr');
89
+ }
90
+ process.stdout.write('✅ Merge explanations present for all resolved files.\n');
79
91
  }
80
- (0, git_exec_1.runGitChecked)(['branch', '-m', base], 'Failed to rename squash branch to the feature name');
81
- const renameEvent = new rules_config_1.BranchMutationEvent(verb, 'RENAME');
82
- renameEvent.fromBranch = ctx.currentBranch;
83
- renameEvent.toBranch = base;
84
- (0, rules_config_1.logBranchMutation)(repoRoot, renameEvent);
85
- // Branch now contains origin/main — stamp a clean main-sync status so the feature-branch-guard
86
- // unblocks edits immediately (no wait for the async refresher).
87
- (0, rules_config_1.stampCleanMainSyncStatus)((0, child_process_1.execSync)('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim());
88
- // Clean sync → the pre-merge snapshot was never needed; delete it (LAST, so any earlier finalize
89
- // failure above keeps it). Conflict sync → keep it as the undo point + trail.
90
- const backupKept = hadConflict;
91
- if (!backupKept && localBranchExists(ctx.backupBranch)) {
92
- (0, git_exec_1.runGitChecked)(['branch', '-D', ctx.backupBranch], 'Failed to delete clean-merge backup');
92
+ localBranchExists(name) {
93
+ return (0, child_process_1.spawnSync)('git', ['show-ref', '--verify', '--quiet', `refs/heads/${name}`]).status === 0;
93
94
  }
94
- const finalizeEvent = new rules_config_1.BranchMutationEvent(verb, 'FINALIZE');
95
- finalizeEvent.fromBranch = ctx.currentBranch;
96
- finalizeEvent.toBranch = base;
97
- finalizeEvent.outcome = 'finalized';
98
- finalizeEvent.artifacts = [backupKept ? `backup=${ctx.backupBranch}` : `backupDeleted=${ctx.backupBranch}`, `remotePR=${base}`];
99
- (0, rules_config_1.logBranchMutation)(repoRoot, finalizeEvent);
100
- printSyncRecap(base, ctx.backupBranch, ctx.prNumber, remoteExists, backupKept);
101
- }
102
- // The explicit, numbered "here is exactly what I did" recap the AI (and human) reads after a sync.
103
- // The whole point of the branch-name change is that step 4 can now say "same name as remote/PR" —
104
- // there is no confusing local-vs-remote divergence left to explain.
105
- function printSyncRecap(feature, backupBranch, prNumber, pushed, backupKept) {
106
- const remoteLine = pushed
107
- ? `landed back on ${feature} (== origin/${feature}${prNumber ? ` == PR #${prNumber}` : ''} — names match)`
108
- : `landed back on ${feature} (local only — no remote branch yet)`;
109
- const step1 = backupKept
110
- ? `snapshotted your pre-merge state → ${backupBranch} (kept — this merge had conflicts)`
111
- : `snapshotted your pre-merge state → ${backupBranch} (auto-removed — clean merge, no undo needed)`;
112
- const trailer = backupKept
113
- ? ` Pre-merge snapshot trail: git branch --list '${feature}PreMerge*'\n` +
114
- ` Its conflict context lives in the paired merge-<n>/ folder under .webpieces/merge-info/\n` +
115
- ` Prune this run's snapshot when safe: git branch -D ${backupBranch}\n\n`
116
- : '\n';
117
- process.stdout.write('\n' + SEP + '✅ Sync complete — here is exactly what I did\n' + SEP + '\n' +
118
- ` 1. ${step1}\n` +
119
- ` 2. pulled origin/main\n` +
120
- ` 3. squash-merged your work onto main\n` +
121
- ` 4. ${remoteLine}\n\n` +
122
- trailer);
123
- }
124
- /**
125
- * Complete a 3-point squash merge. `conflictedFiles` non-null means a conflict was resolved by the AI
126
- * and must be validated + committed before finalizing; null means a clean merge that merge-START
127
- * already committed (finalize only). Either way the merge ends fully finalized on the feature branch.
128
- */
129
- async function mergeEnd(repoRoot, verb, mergeDir, ctx, conflictedFiles) {
130
- if (conflictedFiles !== null) {
131
- process.stdout.write('\n' + SEP + '🔎 Validating Merge Resolution\n' + SEP + '\n');
132
- validateResolution(repoRoot, mergeDir, conflictedFiles);
133
- // Stage the AI's resolved conflicts, but NEVER sweep untracked files into the squash commit
134
- // (a blanket `git add -A` once swept a stale untracked dir in). Fail on untracked so the AI
135
- // commits or deletes them explicitly; then `git add -u` stages tracked resolutions only.
136
- (0, git_exec_1.assertNoUntracked)(repoRoot);
137
- (0, git_exec_1.runGitChecked)(['add', '-u'], 'Failed to stage resolved files');
138
- const nothingStaged = (0, child_process_1.spawnSync)('git', ['diff-index', '--quiet', '--cached', 'HEAD', '--']).status === 0;
139
- if (!nothingStaged) {
140
- (0, git_exec_1.runGitChecked)(['commit', '-m', `Squash merge of ${ctx.currentBranch} (conflicts resolved)`], 'Failed to commit resolved merge');
95
+ // Force-push the squash branch to the stable feature branch (where the single PR lives), then RENAME
96
+ // the local squash branch back to that SAME feature name. On a CLEAN sync the pre-merge snapshot is
97
+ // disposable, so it's deleted at the very end; a CONFLICT sync keeps it.
98
+ finalizeBranch(repoRoot, verb, ctx, hadConflict) {
99
+ process.stdout.write('\n' + SEP + '🗑️ Finalizing\n' + SEP + '\n');
100
+ const base = this.branchNaming.baseBranchName(ctx.currentBranch);
101
+ this.gitExec.runGitChecked(['branch', '-D', ctx.currentBranch], 'Failed to delete old feature branch');
102
+ const remoteExists = (0, child_process_1.spawnSync)('git', ['ls-remote', '--exit-code', '--heads', 'origin', base]).status === 0;
103
+ if (remoteExists) {
104
+ process.stdout.write(ctx.prNumber ? `Updating PR #${ctx.prNumber} (force-with-lease)...\n` : 'Updating remote branch (force-with-lease)...\n');
105
+ this.gitExec.runGitChecked(['push', '-u', '--force-with-lease', 'origin', `${ctx.squashBranch}:${base}`], 'Failed to push to origin');
106
+ }
107
+ else {
108
+ process.stdout.write('No remote branch local only.\n');
141
109
  }
142
- fs.writeFileSync(path.join(mergeDir, 'conflicts-resolved'), '');
143
- process.stdout.write('\n✅ Merge validated and committed.\n');
110
+ this.gitExec.runGitChecked(['checkout', ctx.squashBranch], 'Failed to checkout squash branch');
111
+ // Free the rename target: `base` is normally the branch we just deleted, but a backward-compat
112
+ // sync from a leftover `…wpN` can leave a separate stale `base` lingering — drop it too.
113
+ if (base !== ctx.currentBranch && this.localBranchExists(base)) {
114
+ this.gitExec.runGitChecked(['branch', '-D', base], 'Failed to delete stale base branch');
115
+ }
116
+ this.gitExec.runGitChecked(['branch', '-m', base], 'Failed to rename squash branch to the feature name');
117
+ const renameEvent = new rules_config_1.BranchMutationEvent(verb, 'RENAME');
118
+ renameEvent.fromBranch = ctx.currentBranch;
119
+ renameEvent.toBranch = base;
120
+ (0, rules_config_1.logBranchMutation)(repoRoot, renameEvent);
121
+ // Branch now contains origin/main — stamp a clean main-sync status so the feature-branch-guard
122
+ // unblocks edits immediately (no wait for the async refresher).
123
+ (0, rules_config_1.stampCleanMainSyncStatus)((0, child_process_1.execSync)('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim());
124
+ // Clean sync → the pre-merge snapshot was never needed; delete it LAST. Conflict sync → keep it.
125
+ const backupKept = hadConflict;
126
+ if (!backupKept && this.localBranchExists(ctx.backupBranch)) {
127
+ this.gitExec.runGitChecked(['branch', '-D', ctx.backupBranch], 'Failed to delete clean-merge backup');
128
+ }
129
+ const finalizeEvent = new rules_config_1.BranchMutationEvent(verb, 'FINALIZE');
130
+ finalizeEvent.fromBranch = ctx.currentBranch;
131
+ finalizeEvent.toBranch = base;
132
+ finalizeEvent.outcome = 'finalized';
133
+ finalizeEvent.artifacts = [backupKept ? `backup=${ctx.backupBranch}` : `backupDeleted=${ctx.backupBranch}`, `remotePR=${base}`];
134
+ (0, rules_config_1.logBranchMutation)(repoRoot, finalizeEvent);
135
+ this.printSyncRecap(base, ctx.backupBranch, ctx.prNumber, remoteExists, backupKept);
136
+ }
137
+ // The explicit, numbered "here is exactly what I did" recap the AI (and human) reads after a sync.
138
+ printSyncRecap(feature, backupBranch, prNumber, pushed, backupKept) {
139
+ const remoteLine = pushed
140
+ ? `landed back on ${feature} (== origin/${feature}${prNumber ? ` == PR #${prNumber}` : ''} — names match)`
141
+ : `landed back on ${feature} (local only — no remote branch yet)`;
142
+ const step1 = backupKept
143
+ ? `snapshotted your pre-merge state → ${backupBranch} (kept — this merge had conflicts)`
144
+ : `snapshotted your pre-merge state → ${backupBranch} (auto-removed — clean merge, no undo needed)`;
145
+ const trailer = backupKept
146
+ ? ` Pre-merge snapshot trail: git branch --list '${feature}PreMerge*'\n` +
147
+ ` Its conflict context lives in the paired merge-<n>/ folder under .webpieces/merge-info/\n` +
148
+ ` Prune this run's snapshot when safe: git branch -D ${backupBranch}\n\n`
149
+ : '\n';
150
+ process.stdout.write('\n' + SEP + '✅ Sync complete — here is exactly what I did\n' + SEP + '\n' +
151
+ ` 1. ${step1}\n` +
152
+ ` 2. pulled origin/main\n` +
153
+ ` 3. squash-merged your work onto main\n` +
154
+ ` 4. ${remoteLine}\n\n` +
155
+ trailer);
144
156
  }
145
- // A marker in THIS run dir means the sync hit conflicts (marker is written only on hand-back) —
146
- // correct even for the validated-resume path (conflictedFiles=null yet a conflict). Read it BEFORE
147
- // clearMergeMarker so finalize knows whether to keep the pre-merge snapshot.
148
- const hadConflict = (0, merge_state_1.readMergeMarker)(mergeDir) !== null;
149
- finalizeBranch(repoRoot, verb, ctx, hadConflict);
150
- (0, merge_state_1.clearMergeMarker)(mergeDir);
151
- await (0, cleanTmp_1.cleanTmp)();
152
- }
157
+ };
158
+ exports.MergeEnd = MergeEnd;
159
+ exports.MergeEnd = MergeEnd = tslib_1.__decorate([
160
+ (0, core_context_1.provideSingleton)(),
161
+ (0, inversify_1.injectable)(),
162
+ tslib_1.__metadata("design:paramtypes", [branch_naming_1.BranchNaming,
163
+ cleanTmp_1.CleanTmp,
164
+ git_exec_1.GitExec,
165
+ merge_state_1.MergeState])
166
+ ], MergeEnd);
153
167
  //# sourceMappingURL=merge-end.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"merge-end.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/merge-end.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 MERGE_EXPLANATION_FILE, stampCleanMainSyncStatus, CliExitError,\n MutationVerb, BranchMutationEvent, logBranchMutation,\n} from '@webpieces/rules-config';\nimport { baseBranchName } from './branch-naming';\nimport { cleanTmp } from './cleanTmp';\nimport { assertNoUntracked, runGitChecked } from './git-exec';\nimport { MergeContext } from './merge-start';\nimport { clearMergeMarker, readMergeMarker, perFileContextDir, scanConflictMarkers, scanMergeExplanations } from './merge-state';\n\n// merge-END: the second half of the 3-point squash-merge lifecycle, symmetric with merge-START. Given\n// the branch context, it (optionally) validates + commits the AI's conflict resolution, then ALWAYS\n// finalizes the merge — force-pushes `<branch>Squash` to the stable feature branch and renames it\n// BACK to that same feature name (local == remote == PR head), stamps a clean main-sync status,\n// clears the marker and sweeps stale tmp. The shared runUpdateFromMain (clean path / validated resume), wp-finish-update, and\n// wp-finish-upsert-pr (conflict resolution) all call THIS, so finalization happens in exactly one\n// place and the conflict path can no longer post a PR from the un-swapped squash branch.\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n// Validate the AI's resolution of the conflicted files — the part of the process the AI owns\n// (branch creation/finalization is the script's job, so it is not re-checked here). Throws\n// CliExitError with a fix instruction on any failure; returns only when all three checks pass.\nfunction validateResolution(repoRoot: string, mergeDir: string, conflictedFiles: string[]): void {\n // 1. Scoped conflict-marker scan (only the conflicted files — O(conflicts), not O(repo)).\n const scan = scanConflictMarkers(repoRoot, conflictedFiles);\n if (!scan.clean) {\n throw new CliExitError(1,\n '❌ Unresolved conflict markers (<<<<<<< / ======= / >>>>>>>) remain in:\\n' +\n scan.filesWithMarkers.map((file: string): string => ` - ${file}`).join('\\n') +\n '\\n\\nResolve them, then re-run: pnpm wp-finish-upsert-pr',\n );\n }\n\n // 2. Ensure git itself has no remaining unmerged entries.\n const unmerged = execSync('git diff --name-only --diff-filter=U', { encoding: 'utf8' }).trim();\n if (unmerged !== '') {\n throw new CliExitError(1,\n '❌ Git still reports unmerged files:\\n' + unmerged +\n '\\n\\nResolve and `git add` them, then re-run: pnpm wp-finish-upsert-pr',\n );\n }\n process.stdout.write('✅ No conflict markers in resolved files.\\n');\n\n // 3. Explanation check — every conflicted file must have a non-empty merge-explanation.md in\n // its per-file context dir, proving the AI deliberately 3-point merged it (and recording how)\n // rather than blindly taking one side. A sidecar file works for any type, incl. JSON/deletes.\n const explanations = scanMergeExplanations(mergeDir, conflictedFiles);\n if (!explanations.clean) {\n const missing = explanations.filesWithMarkers\n .map((file: string): string => ` - ${file}\\n → ${path.join(perFileContextDir(mergeDir, file), MERGE_EXPLANATION_FILE)}`)\n .join('\\n');\n throw new CliExitError(1,\n `❌ Missing/empty merge explanation (${MERGE_EXPLANATION_FILE}) for:\\n` +\n missing +\n '\\n\\nWrite a few sentences on how you resolved each (which side, what you combined, why),\\n' +\n 'then re-run: pnpm wp-finish-upsert-pr',\n );\n }\n process.stdout.write('✅ Merge explanations present for all resolved files.\\n');\n}\n\nfunction localBranchExists(name: string): boolean {\n return spawnSync('git', ['show-ref', '--verify', '--quiet', `refs/heads/${name}`]).status === 0;\n}\n\n// Force-push the squash branch to the stable feature branch (where the single PR lives), then RENAME\n// the local squash branch back to that SAME feature name — so the local branch, the remote branch,\n// and the PR head are all one name (no `wpN` divergence). On a CLEAN sync (`!hadConflict`) the\n// pre-merge snapshot is disposable, so it's deleted at the very end; a CONFLICT sync keeps it (paired\n// with its `merge-<n>/` context). Ends with an explicit \"here is exactly what I did\" recap.\nfunction finalizeBranch(repoRoot: string, verb: MutationVerb, ctx: MergeContext, hadConflict: boolean): void {\n process.stdout.write('\\n' + SEP + '🗑️ Finalizing\\n' + SEP + '\\n');\n const base = baseBranchName(ctx.currentBranch);\n runGitChecked(['branch', '-D', ctx.currentBranch], 'Failed to delete old feature branch');\n\n const remoteExists = spawnSync('git', ['ls-remote', '--exit-code', '--heads', 'origin', base]).status === 0;\n if (remoteExists) {\n process.stdout.write(ctx.prNumber ? `Updating PR #${ctx.prNumber} (force-with-lease)...\\n` : 'Updating remote branch (force-with-lease)...\\n');\n runGitChecked(['push', '-u', '--force-with-lease', 'origin', `${ctx.squashBranch}:${base}`], 'Failed to push to origin');\n } else {\n process.stdout.write('No remote branch — local only.\\n');\n }\n runGitChecked(['checkout', ctx.squashBranch], 'Failed to checkout squash branch');\n // Free the rename target: `base` is normally the branch we just deleted (ctx.currentBranch), but on\n // a backward-compat sync from a leftover `…wpN` a separate stale `base` can linger — drop it too.\n if (base !== ctx.currentBranch && localBranchExists(base)) {\n runGitChecked(['branch', '-D', base], 'Failed to delete stale base branch');\n }\n runGitChecked(['branch', '-m', base], 'Failed to rename squash branch to the feature name');\n const renameEvent = new BranchMutationEvent(verb, 'RENAME');\n renameEvent.fromBranch = ctx.currentBranch;\n renameEvent.toBranch = base;\n logBranchMutation(repoRoot, renameEvent);\n\n // Branch now contains origin/main — stamp a clean main-sync status so the feature-branch-guard\n // unblocks edits immediately (no wait for the async refresher).\n stampCleanMainSyncStatus(execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim());\n\n // Clean sync → the pre-merge snapshot was never needed; delete it (LAST, so any earlier finalize\n // failure above keeps it). Conflict sync → keep it as the undo point + trail.\n const backupKept = hadConflict;\n if (!backupKept && localBranchExists(ctx.backupBranch)) {\n runGitChecked(['branch', '-D', ctx.backupBranch], 'Failed to delete clean-merge backup');\n }\n\n const finalizeEvent = new BranchMutationEvent(verb, 'FINALIZE');\n finalizeEvent.fromBranch = ctx.currentBranch;\n finalizeEvent.toBranch = base;\n finalizeEvent.outcome = 'finalized';\n finalizeEvent.artifacts = [backupKept ? `backup=${ctx.backupBranch}` : `backupDeleted=${ctx.backupBranch}`, `remotePR=${base}`];\n logBranchMutation(repoRoot, finalizeEvent);\n\n printSyncRecap(base, ctx.backupBranch, ctx.prNumber, remoteExists, backupKept);\n}\n\n// The explicit, numbered \"here is exactly what I did\" recap the AI (and human) reads after a sync.\n// The whole point of the branch-name change is that step 4 can now say \"same name as remote/PR\" —\n// there is no confusing local-vs-remote divergence left to explain.\nfunction printSyncRecap(feature: string, backupBranch: string, prNumber: string, pushed: boolean, backupKept: boolean): void {\n const remoteLine = pushed\n ? `landed back on ${feature} (== origin/${feature}${prNumber ? ` == PR #${prNumber}` : ''} — names match)`\n : `landed back on ${feature} (local only — no remote branch yet)`;\n const step1 = backupKept\n ? `snapshotted your pre-merge state → ${backupBranch} (kept — this merge had conflicts)`\n : `snapshotted your pre-merge state → ${backupBranch} (auto-removed — clean merge, no undo needed)`;\n const trailer = backupKept\n ? ` Pre-merge snapshot trail: git branch --list '${feature}PreMerge*'\\n` +\n ` Its conflict context lives in the paired merge-<n>/ folder under .webpieces/merge-info/\\n` +\n ` Prune this run's snapshot when safe: git branch -D ${backupBranch}\\n\\n`\n : '\\n';\n process.stdout.write(\n '\\n' + SEP + '✅ Sync complete — here is exactly what I did\\n' + SEP + '\\n' +\n ` 1. ${step1}\\n` +\n ` 2. pulled origin/main\\n` +\n ` 3. squash-merged your work onto main\\n` +\n ` 4. ${remoteLine}\\n\\n` +\n trailer,\n );\n}\n\n/**\n * Complete a 3-point squash merge. `conflictedFiles` non-null means a conflict was resolved by the AI\n * and must be validated + committed before finalizing; null means a clean merge that merge-START\n * already committed (finalize only). Either way the merge ends fully finalized on the feature branch.\n */\nexport async function mergeEnd(\n repoRoot: string, verb: MutationVerb, mergeDir: string, ctx: MergeContext, conflictedFiles: string[] | null,\n): Promise<void> {\n if (conflictedFiles !== null) {\n process.stdout.write('\\n' + SEP + '🔎 Validating Merge Resolution\\n' + SEP + '\\n');\n validateResolution(repoRoot, mergeDir, conflictedFiles);\n // Stage the AI's resolved conflicts, but NEVER sweep untracked files into the squash commit\n // (a blanket `git add -A` once swept a stale untracked dir in). Fail on untracked so the AI\n // commits or deletes them explicitly; then `git add -u` stages tracked resolutions only.\n assertNoUntracked(repoRoot);\n runGitChecked(['add', '-u'], 'Failed to stage resolved files');\n\n const nothingStaged = spawnSync('git', ['diff-index', '--quiet', '--cached', 'HEAD', '--']).status === 0;\n if (!nothingStaged) {\n runGitChecked(\n ['commit', '-m', `Squash merge of ${ctx.currentBranch} (conflicts resolved)`],\n 'Failed to commit resolved merge',\n );\n }\n fs.writeFileSync(path.join(mergeDir, 'conflicts-resolved'), '');\n process.stdout.write('\\n✅ Merge validated and committed.\\n');\n }\n\n // A marker in THIS run dir means the sync hit conflicts (marker is written only on hand-back) —\n // correct even for the validated-resume path (conflictedFiles=null yet a conflict). Read it BEFORE\n // clearMergeMarker so finalize knows whether to keep the pre-merge snapshot.\n const hadConflict = readMergeMarker(mergeDir) !== null;\n finalizeBranch(repoRoot, verb, ctx, hadConflict);\n clearMergeMarker(mergeDir);\n await cleanTmp();\n}\n"]}
1
+ {"version":3,"file":"merge-end.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/merge-end.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 MERGE_EXPLANATION_FILE, stampCleanMainSyncStatus, CliExitError,\n MutationVerb, BranchMutationEvent, logBranchMutation,\n} from '@webpieces/rules-config';\nimport { provideSingleton } from '@webpieces/core-context';\nimport { injectable } from 'inversify';\nimport { BranchNaming } from './branch-naming';\nimport { CleanTmp } from './cleanTmp';\nimport { GitExec } from './git-exec';\nimport { MergeContext } from './merge-start';\nimport { MergeState } from './merge-state';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n// merge-END: the second half of the 3-point squash-merge lifecycle, symmetric with merge-START. Given\n// the branch context, it (optionally) validates + commits the AI's conflict resolution, then ALWAYS\n// finalizes — force-pushes `<branch>Squash` to the stable feature branch and renames it BACK to that\n// same feature name (local == remote == PR head), stamps a clean main-sync status, clears the marker,\n// and sweeps stale tmp. RunUpdate (clean path / validated resume), wp-finish-update, and\n// wp-finish-upsert-pr (conflict resolution) all call THIS, so finalization happens in exactly one place.\n@provideSingleton()\n@injectable()\nexport class MergeEnd {\n constructor(\n private readonly branchNaming: BranchNaming,\n private readonly cleanTmpService: CleanTmp,\n private readonly gitExec: GitExec,\n private readonly mergeState: MergeState,\n ) {}\n\n /**\n * Complete a 3-point squash merge. `conflictedFiles` non-null means a conflict was resolved by the\n * AI and must be validated + committed before finalizing; null means a clean merge that merge-START\n * already committed (finalize only). Either way the merge ends fully finalized on the feature branch.\n */\n async mergeEnd(\n repoRoot: string, verb: MutationVerb, mergeDir: string, ctx: MergeContext, conflictedFiles: string[] | null,\n ): Promise<void> {\n if (conflictedFiles !== null) {\n process.stdout.write('\\n' + SEP + '🔎 Validating Merge Resolution\\n' + SEP + '\\n');\n this.validateResolution(repoRoot, mergeDir, conflictedFiles);\n // Stage the AI's resolved conflicts, but NEVER sweep untracked files into the squash commit.\n // Fail on untracked so the AI commits or deletes them explicitly; then `git add -u` stages\n // tracked resolutions only.\n this.gitExec.assertNoUntracked(repoRoot);\n this.gitExec.runGitChecked(['add', '-u'], 'Failed to stage resolved files');\n\n const nothingStaged = spawnSync('git', ['diff-index', '--quiet', '--cached', 'HEAD', '--']).status === 0;\n if (!nothingStaged) {\n this.gitExec.runGitChecked(\n ['commit', '-m', `Squash merge of ${ctx.currentBranch} (conflicts resolved)`],\n 'Failed to commit resolved merge',\n );\n }\n fs.writeFileSync(path.join(mergeDir, 'conflicts-resolved'), '');\n process.stdout.write('\\n✅ Merge validated and committed.\\n');\n }\n\n // A marker in THIS run dir means the sync hit conflicts (marker is written only on hand-back).\n // Read it BEFORE clearMergeMarker so finalize knows whether to keep the pre-merge snapshot.\n const hadConflict = this.mergeState.readMergeMarker(mergeDir) !== null;\n this.finalizeBranch(repoRoot, verb, ctx, hadConflict);\n this.mergeState.clearMergeMarker(mergeDir);\n await this.cleanTmpService.cleanTmp();\n }\n\n // Validate the AI's resolution of the conflicted files. Throws CliExitError with a fix instruction\n // on any failure; returns only when all three checks pass.\n private validateResolution(repoRoot: string, mergeDir: string, conflictedFiles: string[]): void {\n // 1. Scoped conflict-marker scan (only the conflicted files — O(conflicts), not O(repo)).\n const scan = this.mergeState.scanConflictMarkers(repoRoot, conflictedFiles);\n if (!scan.clean) {\n throw new CliExitError(1,\n '❌ Unresolved conflict markers (<<<<<<< / ======= / >>>>>>>) remain in:\\n' +\n scan.filesWithMarkers.map((file: string): string => ` - ${file}`).join('\\n') +\n '\\n\\nResolve them, then re-run: pnpm wp-finish-upsert-pr',\n );\n }\n\n // 2. Ensure git itself has no remaining unmerged entries.\n const unmerged = execSync('git diff --name-only --diff-filter=U', { encoding: 'utf8' }).trim();\n if (unmerged !== '') {\n throw new CliExitError(1,\n '❌ Git still reports unmerged files:\\n' + unmerged +\n '\\n\\nResolve and `git add` them, then re-run: pnpm wp-finish-upsert-pr',\n );\n }\n process.stdout.write('✅ No conflict markers in resolved files.\\n');\n\n // 3. Explanation check — every conflicted file must have a non-empty merge-explanation.md in its\n // per-file context dir, proving the AI deliberately 3-point merged it (and recording how).\n const explanations = this.mergeState.scanMergeExplanations(mergeDir, conflictedFiles);\n if (!explanations.clean) {\n const missing = explanations.filesWithMarkers\n .map((file: string): string => ` - ${file}\\n → ${path.join(this.mergeState.perFileContextDir(mergeDir, file), MERGE_EXPLANATION_FILE)}`)\n .join('\\n');\n throw new CliExitError(1,\n `❌ Missing/empty merge explanation (${MERGE_EXPLANATION_FILE}) for:\\n` +\n missing +\n '\\n\\nWrite a few sentences on how you resolved each (which side, what you combined, why),\\n' +\n 'then re-run: pnpm wp-finish-upsert-pr',\n );\n }\n process.stdout.write('✅ Merge explanations present for all resolved files.\\n');\n }\n\n private localBranchExists(name: string): boolean {\n return spawnSync('git', ['show-ref', '--verify', '--quiet', `refs/heads/${name}`]).status === 0;\n }\n\n // Force-push the squash branch to the stable feature branch (where the single PR lives), then RENAME\n // the local squash branch back to that SAME feature name. On a CLEAN sync the pre-merge snapshot is\n // disposable, so it's deleted at the very end; a CONFLICT sync keeps it.\n private finalizeBranch(repoRoot: string, verb: MutationVerb, ctx: MergeContext, hadConflict: boolean): void {\n process.stdout.write('\\n' + SEP + '🗑️ Finalizing\\n' + SEP + '\\n');\n const base = this.branchNaming.baseBranchName(ctx.currentBranch);\n this.gitExec.runGitChecked(['branch', '-D', ctx.currentBranch], 'Failed to delete old feature branch');\n\n const remoteExists = spawnSync('git', ['ls-remote', '--exit-code', '--heads', 'origin', base]).status === 0;\n if (remoteExists) {\n process.stdout.write(ctx.prNumber ? `Updating PR #${ctx.prNumber} (force-with-lease)...\\n` : 'Updating remote branch (force-with-lease)...\\n');\n this.gitExec.runGitChecked(['push', '-u', '--force-with-lease', 'origin', `${ctx.squashBranch}:${base}`], 'Failed to push to origin');\n } else {\n process.stdout.write('No remote branch — local only.\\n');\n }\n this.gitExec.runGitChecked(['checkout', ctx.squashBranch], 'Failed to checkout squash branch');\n // Free the rename target: `base` is normally the branch we just deleted, but a backward-compat\n // sync from a leftover `…wpN` can leave a separate stale `base` lingering — drop it too.\n if (base !== ctx.currentBranch && this.localBranchExists(base)) {\n this.gitExec.runGitChecked(['branch', '-D', base], 'Failed to delete stale base branch');\n }\n this.gitExec.runGitChecked(['branch', '-m', base], 'Failed to rename squash branch to the feature name');\n const renameEvent = new BranchMutationEvent(verb, 'RENAME');\n renameEvent.fromBranch = ctx.currentBranch;\n renameEvent.toBranch = base;\n logBranchMutation(repoRoot, renameEvent);\n\n // Branch now contains origin/main — stamp a clean main-sync status so the feature-branch-guard\n // unblocks edits immediately (no wait for the async refresher).\n stampCleanMainSyncStatus(execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim());\n\n // Clean sync → the pre-merge snapshot was never needed; delete it LAST. Conflict sync → keep it.\n const backupKept = hadConflict;\n if (!backupKept && this.localBranchExists(ctx.backupBranch)) {\n this.gitExec.runGitChecked(['branch', '-D', ctx.backupBranch], 'Failed to delete clean-merge backup');\n }\n\n const finalizeEvent = new BranchMutationEvent(verb, 'FINALIZE');\n finalizeEvent.fromBranch = ctx.currentBranch;\n finalizeEvent.toBranch = base;\n finalizeEvent.outcome = 'finalized';\n finalizeEvent.artifacts = [backupKept ? `backup=${ctx.backupBranch}` : `backupDeleted=${ctx.backupBranch}`, `remotePR=${base}`];\n logBranchMutation(repoRoot, finalizeEvent);\n\n this.printSyncRecap(base, ctx.backupBranch, ctx.prNumber, remoteExists, backupKept);\n }\n\n // The explicit, numbered \"here is exactly what I did\" recap the AI (and human) reads after a sync.\n private printSyncRecap(feature: string, backupBranch: string, prNumber: string, pushed: boolean, backupKept: boolean): void {\n const remoteLine = pushed\n ? `landed back on ${feature} (== origin/${feature}${prNumber ? ` == PR #${prNumber}` : ''} — names match)`\n : `landed back on ${feature} (local only — no remote branch yet)`;\n const step1 = backupKept\n ? `snapshotted your pre-merge state → ${backupBranch} (kept — this merge had conflicts)`\n : `snapshotted your pre-merge state → ${backupBranch} (auto-removed — clean merge, no undo needed)`;\n const trailer = backupKept\n ? ` Pre-merge snapshot trail: git branch --list '${feature}PreMerge*'\\n` +\n ` Its conflict context lives in the paired merge-<n>/ folder under .webpieces/merge-info/\\n` +\n ` Prune this run's snapshot when safe: git branch -D ${backupBranch}\\n\\n`\n : '\\n';\n process.stdout.write(\n '\\n' + SEP + '✅ Sync complete — here is exactly what I did\\n' + SEP + '\\n' +\n ` 1. ${step1}\\n` +\n ` 2. pulled origin/main\\n` +\n ` 3. squash-merged your work onto main\\n` +\n ` 4. ${remoteLine}\\n\\n` +\n trailer,\n );\n }\n}\n"]}
@@ -1,4 +1,8 @@
1
1
  import { MutationVerb } from '@webpieces/rules-config';
2
+ import { GatherInfo } from '../git-gatherInfo';
3
+ import { BranchNaming } from './branch-naming';
4
+ import { GitExec } from './git-exec';
5
+ import { MergeState } from './merge-state';
2
6
  export declare class MergeContext {
3
7
  currentBranch: string;
4
8
  squashBranch: string;
@@ -12,4 +16,21 @@ export declare class MergeStartResult {
12
16
  runDir: string;
13
17
  constructor(status: 'clean' | 'conflict', context: MergeContext | null, runDir: string);
14
18
  }
15
- export declare function mergeStart(repoRoot: string, verb: MutationVerb, home: string, finishCommand: string): Promise<MergeStartResult>;
19
+ export declare class MergeStart {
20
+ private readonly gatherInfo;
21
+ private readonly branchNaming;
22
+ private readonly gitExec;
23
+ private readonly mergeState;
24
+ constructor(gatherInfo: GatherInfo, branchNaming: BranchNaming, gitExec: GitExec, mergeState: MergeState);
25
+ mergeStart(repoRoot: string, verb: MutationVerb, home: string, finishCommand: string): Promise<MergeStartResult>;
26
+ private detectPr;
27
+ private chooseSyncSlot;
28
+ private createBackup;
29
+ private saveConflictContext;
30
+ private mergeProcessDoc;
31
+ private writeMergeProcessDoc;
32
+ private printConflictHandback;
33
+ private handleConflictsHandback;
34
+ private shortSha;
35
+ private logConflict;
36
+ }