@webpieces/pr-gate 0.3.291 → 0.3.293

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/pr-gate",
3
- "version": "0.3.291",
3
+ "version": "0.3.293",
4
4
  "description": "Gated PR system: 3-point squash-merge, merge validation gate, and red/yellow/green PR dashboard. Standalone scripts, no Nx dependency required.",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -23,7 +23,7 @@
23
23
  "directory": "packages/tooling/pr-gate"
24
24
  },
25
25
  "dependencies": {
26
- "@webpieces/rules-config": "0.3.291"
26
+ "@webpieces/rules-config": "0.3.293"
27
27
  },
28
28
  "publishConfig": {
29
29
  "access": "public"
@@ -26,7 +26,15 @@ function gitOut(args) {
26
26
  const result = (0, child_process_1.spawnSync)('git', args, { encoding: 'utf8' });
27
27
  return result.status === 0 ? (result.stdout ?? '').trim() : '';
28
28
  }
29
- function buildDashboard(repoRoot, buildPassed, review) {
29
+ // The user-facing PR title: the AI-authored review.title, or — if the AI omitted it — a readable
30
+ // fallback derived from the stable feature name (NEVER the internal `Squash merge of <branch>` commit
31
+ // subject, which leaked bookkeeping into the PR title).
32
+ function prTitleFrom(review) {
33
+ if (review.title !== '')
34
+ return review.title;
35
+ return (0, git_readAiBranchName_1.getFeatureName)().replace(/[-/]+/g, ' ').trim();
36
+ }
37
+ function buildDashboard(repoRoot, buildPassed, review, title) {
30
38
  const config = (0, rules_config_1.loadAndValidate)(repoRoot).prGate;
31
39
  const forkPoint = gitOut(['merge-base', 'origin/main', 'HEAD']);
32
40
  const featureHead = gitOut(['rev-parse', 'HEAD']);
@@ -34,47 +42,49 @@ function buildDashboard(repoRoot, buildPassed, review) {
34
42
  const range = `${forkPoint}..${featureHead}`;
35
43
  const changedFiles = gitOut(['diff', range, '--name-only']).split('\n').filter((f) => f.trim() !== '');
36
44
  const patch = gitOut(['diff', range]);
37
- const title = gitOut(['log', '-1', '--format=%s']);
38
45
  const gateResults = (0, dashboard_1.computeGateResults)(config.gates, changedFiles);
39
46
  const disables = (0, dashboard_1.countAddedDisables)(patch);
40
47
  const input = new dashboard_1.DashboardInput(title, gateResults, disables, buildPassed, forkPoint, featureHead, mainHead, review);
41
48
  return (0, dashboard_1.renderDashboard)(input);
42
49
  }
43
- // The PR always lives on the stable base branch (the local branch may be a numbered generation
44
- // base2/base3/…). Look up / create / merge against `baseBranch`, never the numbered branch, or
45
- // generation 2+ would fail to find its PR and open a duplicate.
46
- function upsertPr(repoRoot, baseBranch, body) {
50
+ // The PR, the remote branch, and the local branch all share the one stable feature name now. Look up /
51
+ // create / merge against `baseBranch` (baseBranchName also tolerates a leftover `…wpN` mid-transition),
52
+ // or a resolve from such a leftover could fail to find its PR and open a duplicate.
53
+ function upsertPr(repoRoot, baseBranch, body, title) {
47
54
  const prDir = (0, rules_config_1.prDirFor)(repoRoot, (0, git_readAiBranchName_1.getFeatureName)());
48
55
  fs.mkdirSync(prDir, { recursive: true });
49
56
  const bodyFile = path.join(prDir, 'pr-body.md');
50
57
  fs.writeFileSync(bodyFile, body + '\n');
51
- const existing = gitOut(['log', '-1', '--format=%s']); // title fallback
52
58
  const prNumber = (0, child_process_1.spawnSync)('gh', ['pr', 'list', '--head', baseBranch, '--json', 'number', '--jq', '.[0].number'], { encoding: 'utf8' });
53
59
  const num = prNumber.status === 0 ? (prNumber.stdout ?? '').trim() : '';
54
60
  if (num === '') {
55
61
  process.stdout.write('Creating PR...\n');
56
- const create = (0, child_process_1.spawnSync)('gh', ['pr', 'create', '--head', baseBranch, '--base', 'main', '--title', existing, '--body-file', bodyFile], { stdio: 'inherit' });
62
+ const create = (0, child_process_1.spawnSync)('gh', ['pr', 'create', '--head', baseBranch, '--base', 'main', '--title', title, '--body-file', bodyFile], { stdio: 'inherit' });
57
63
  if (create.status !== 0) {
58
64
  process.stderr.write('⚠️ gh pr create failed — create the PR manually with the body in:\n ' + bodyFile + '\n');
59
- return;
65
+ return '';
60
66
  }
61
67
  }
62
68
  else {
63
69
  process.stdout.write(`Updating PR #${num}...\n`);
64
- (0, child_process_1.spawnSync)('gh', ['pr', 'edit', num, '--body-file', bodyFile], { stdio: 'inherit' });
70
+ // Keep the title in sync with the latest review.title (not just the body).
71
+ (0, child_process_1.spawnSync)('gh', ['pr', 'edit', num, '--title', title, '--body-file', bodyFile], { stdio: 'inherit' });
65
72
  }
66
73
  (0, child_process_1.spawnSync)('gh', ['pr', 'merge', baseBranch, '--auto', '--squash'], { stdio: 'inherit' });
74
+ return num;
67
75
  }
68
76
  async function main() {
69
77
  const repoRoot = (0, child_process_1.execSync)('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim();
70
78
  // Refresh the AI-facing workflow doc so it's present + current for any failure message to cite.
71
79
  (0, rules_config_1.writeTemplate)(repoRoot, 'webpieces.git-workflow.md');
72
- const mergeDir = (0, merge_state_1.mergeDirFor)(repoRoot, (0, git_readAiBranchName_1.getFeatureName)());
80
+ const home = (0, merge_state_1.mergeDirFor)(repoRoot, (0, git_readAiBranchName_1.getFeatureName)());
73
81
  // 1. Finish any in-progress conflict resolution: validate + commit + finalize the branch swap.
74
- // No marker (or already validated) => no merge in progress => nothing to do (the common case).
75
- const marker = (0, merge_state_1.readMergeMarker)(mergeDir);
76
- if (marker && !marker.validated) {
77
- await (0, merge_end_1.mergeEnd)(repoRoot, 'wp-finish-upsert-pr', mergeDir, new merge_start_1.MergeContext(marker.currentBranch, marker.squashBranch, marker.backupBranch, marker.prNumber), marker.conflictedFiles);
82
+ // No active run dir (or its marker already validated) => no merge in progress => nothing to do
83
+ // (the common case). The active merge is the `merge-<n>/` run dir holding a marker.
84
+ const activeDir = (0, merge_state_1.findActiveMergeRunDir)(home);
85
+ const marker = activeDir ? (0, merge_state_1.readMergeMarker)(activeDir) : null;
86
+ if (activeDir && marker && !marker.validated) {
87
+ await (0, merge_end_1.mergeEnd)(repoRoot, 'wp-finish-upsert-pr', activeDir, new merge_start_1.MergeContext(marker.currentBranch, marker.squashBranch, marker.backupBranch, marker.prNumber), marker.conflictedFiles);
78
88
  }
79
89
  // 2. REQUIRE the AI-authored review.json (throws InformAiError with the schema if missing/invalid).
80
90
  const review = (0, rules_config_1.loadReviewJson)((0, rules_config_1.reviewJsonPath)(repoRoot, (0, git_readAiBranchName_1.getFeatureName)()));
@@ -84,14 +94,20 @@ async function main() {
84
94
  (0, git_exec_1.assertCleanTree)(repoRoot);
85
95
  // 3. Authoritative build gate, then push, then post.
86
96
  (0, build_affected_1.runBuildGate)(repoRoot, new build_affected_1.BuildGateOptions('🛠️ Build gate (authoritative)', 'pnpm wp-finish-upsert-pr', 'Build failed — no PR created/updated.'));
87
- // After finalize the local branch is the numbered generation (base2/…); the remote branch and
88
- // PR live on the stable base name push and upsert against that.
97
+ // After finalize the local branch, the remote branch, and the PR all share the SAME stable name —
98
+ // push and upsert against it. (baseBranchName is a no-op on the already-stable name; it also
99
+ // tolerates a leftover `…wpN` mid-transition.)
89
100
  const base = (0, branch_naming_1.baseBranchName)((0, child_process_1.execSync)('git branch --show-current', { encoding: 'utf8' }).trim());
90
101
  (0, git_exec_1.ensurePushed)(base);
91
102
  process.stdout.write('\n' + SEP + '📋 Dashboard + PR\n' + SEP + '\n');
92
- const body = buildDashboard(repoRoot, true, review);
93
- upsertPr(repoRoot, base, body);
94
- process.stdout.write('\n✅ Done.\n');
103
+ const title = prTitleFrom(review);
104
+ const body = buildDashboard(repoRoot, true, review, title);
105
+ const prNum = upsertPr(repoRoot, base, body, title);
106
+ process.stdout.write('\n' + SEP + '✅ PR finished — here is exactly what I did\n' + SEP + '\n' +
107
+ ` 1. validated the build gate (authoritative)\n` +
108
+ ` 2. force-pushed your work to origin/${base}\n` +
109
+ ` 3. ${prNum ? `updated/created PR #${prNum}` : 'created the PR'} titled: "${title}"\n` +
110
+ ` You are on ${base} — same name as the remote branch and the PR head.\n\n`);
95
111
  }
96
112
  if (require.main === module)
97
113
  (0, rules_config_1.runMain)(main);
@@ -1 +1 @@
1
- {"version":3,"file":"git-finishUpsertPr.js","sourceRoot":"","sources":["../../../../../../packages/tooling/pr-gate/src/scripts/git-finishUpsertPr.ts"],"names":[],"mappings":";;;AAuFA,oBAsCC;;AA5HD,iDAAoD;AACpD,+CAAyB;AACzB,mDAA6B;AAC7B,0DAQiC;AACjC,0EAAiE;AACjE,4DAA0D;AAC1D,kDAAoE;AACpE,8DAA2E;AAC3E,wDAAsE;AACtE,oDAAgD;AAChD,wDAAsD;AACtD,sDAKgC;AAEhC,sGAAsG;AACtG,uGAAuG;AACvG,uGAAuG;AACvG,8FAA8F;AAC9F,qGAAqG;AACrG,+BAA+B;AAE/B,MAAM,GAAG,GAAG,0DAA0D,CAAC;AAEvE,SAAS,MAAM,CAAC,IAAc;IAC1B,MAAM,MAAM,GAAG,IAAA,yBAAS,EAAC,KAAK,EAAE,IAAI,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC;IAC5D,OAAO,MAAM,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;AACnE,CAAC;AAED,SAAS,cAAc,CAAC,QAAgB,EAAE,WAAoB,EAAE,MAAkB;IAC9E,MAAM,MAAM,GAAG,IAAA,8BAAe,EAAC,QAAQ,CAAC,CAAC,MAAM,CAAC;IAChD,MAAM,SAAS,GAAG,MAAM,CAAC,CAAC,YAAY,EAAE,aAAa,EAAE,MAAM,CAAC,CAAC,CAAC;IAChE,MAAM,WAAW,GAAG,MAAM,CAAC,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC;IAClD,MAAM,QAAQ,GAAG,MAAM,CAAC,CAAC,WAAW,EAAE,aAAa,CAAC,CAAC,CAAC;IACtD,MAAM,KAAK,GAAG,GAAG,SAAS,KAAK,WAAW,EAAE,CAAC;IAC7C,MAAM,YAAY,GAAG,MAAM,CAAC,CAAC,MAAM,EAAE,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,CAAS,EAAW,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IACxH,MAAM,KAAK,GAAG,MAAM,CAAC,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC;IACtC,MAAM,KAAK,GAAG,MAAM,CAAC,CAAC,KAAK,EAAE,IAAI,EAAE,aAAa,CAAC,CAAC,CAAC;IAEnD,MAAM,WAAW,GAAG,IAAA,8BAAkB,EAAC,MAAM,CAAC,KAAK,EAAE,YAAY,CAAC,CAAC;IACnE,MAAM,QAAQ,GAAG,IAAA,8BAAkB,EAAC,KAAK,CAAC,CAAC;IAC3C,MAAM,KAAK,GAAG,IAAI,0BAAc,CAAC,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,WAAW,EAAE,SAAS,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC;IACtH,OAAO,IAAA,2BAAe,EAAC,KAAK,CAAC,CAAC;AAClC,CAAC;AAED,+FAA+F;AAC/F,+FAA+F;AAC/F,gEAAgE;AAChE,SAAS,QAAQ,CAAC,QAAgB,EAAE,UAAkB,EAAE,IAAY;IAChE,MAAM,KAAK,GAAG,IAAA,uBAAQ,EAAC,QAAQ,EAAE,IAAA,qCAAc,GAAE,CAAC,CAAC;IACnD,EAAE,CAAC,SAAS,CAAC,KAAK,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACzC,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,YAAY,CAAC,CAAC;IAChD,EAAE,CAAC,aAAa,CAAC,QAAQ,EAAE,IAAI,GAAG,IAAI,CAAC,CAAC;IAExC,MAAM,QAAQ,GAAG,MAAM,CAAC,CAAC,KAAK,EAAE,IAAI,EAAE,aAAa,CAAC,CAAC,CAAC,CAAC,iBAAiB;IACxE,MAAM,QAAQ,GAAG,IAAA,yBAAS,EACtB,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,aAAa,CAAC,EACrF,EAAE,QAAQ,EAAE,MAAM,EAAE,CACvB,CAAC;IACF,MAAM,GAAG,GAAG,QAAQ,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAExE,IAAI,GAAG,KAAK,EAAE,EAAE,CAAC;QACb,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,kBAAkB,CAAC,CAAC;QACzC,MAAM,MAAM,GAAG,IAAA,yBAAS,EAAC,IAAI,EAAE,CAAC,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,aAAa,EAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;QAC7J,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACtB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,wEAAwE,GAAG,QAAQ,GAAG,IAAI,CAAC,CAAC;YACjH,OAAO;QACX,CAAC;IACL,CAAC;SAAM,CAAC;QACJ,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,gBAAgB,GAAG,OAAO,CAAC,CAAC;QACjD,IAAA,yBAAS,EAAC,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,aAAa,EAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;IACxF,CAAC;IACD,IAAA,yBAAS,EAAC,IAAI,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,UAAU,CAAC,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;AAC7F,CAAC;AAEM,KAAK,UAAU,IAAI;IACtB,MAAM,QAAQ,GAAG,IAAA,wBAAQ,EAAC,+BAA+B,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IACxF,gGAAgG;IAChG,IAAA,4BAAa,EAAC,QAAQ,EAAE,2BAA2B,CAAC,CAAC;IACrD,MAAM,QAAQ,GAAG,IAAA,yBAAW,EAAC,QAAQ,EAAE,IAAA,qCAAc,GAAE,CAAC,CAAC;IAEzD,+FAA+F;IAC/F,kGAAkG;IAClG,MAAM,MAAM,GAAG,IAAA,6BAAe,EAAC,QAAQ,CAAC,CAAC;IACzC,IAAI,MAAM,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC;QAC9B,MAAM,IAAA,oBAAQ,EACV,QAAQ,EAAE,qBAAqB,EAAE,QAAQ,EACzC,IAAI,0BAAY,CAAC,MAAM,CAAC,aAAa,EAAE,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,QAAQ,CAAC,EACjG,MAAM,CAAC,eAAe,CACzB,CAAC;IACN,CAAC;IAED,oGAAoG;IACpG,MAAM,MAAM,GAAG,IAAA,6BAAc,EAAC,IAAA,6BAAc,EAAC,QAAQ,EAAE,IAAA,qCAAc,GAAE,CAAC,CAAC,CAAC;IAE1E,mGAAmG;IACnG,kGAAkG;IAClG,8FAA8F;IAC9F,IAAA,0BAAe,EAAC,QAAQ,CAAC,CAAC;IAE1B,qDAAqD;IACrD,IAAA,6BAAY,EAAC,QAAQ,EAAE,IAAI,iCAAgB,CACvC,iCAAiC,EAAE,0BAA0B,EAAE,uCAAuC,CACzG,CAAC,CAAC;IACH,8FAA8F;IAC9F,kEAAkE;IAClE,MAAM,IAAI,GAAG,IAAA,8BAAc,EAAC,IAAA,wBAAQ,EAAC,2BAA2B,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;IAChG,IAAA,uBAAY,EAAC,IAAI,CAAC,CAAC;IAEnB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,GAAG,GAAG,GAAG,qBAAqB,GAAG,GAAG,GAAG,IAAI,CAAC,CAAC;IACtE,MAAM,IAAI,GAAG,cAAc,CAAC,QAAQ,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;IACpD,QAAQ,CAAC,QAAQ,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;IAC/B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC;AACxC,CAAC;AAED,IAAI,OAAO,CAAC,IAAI,KAAK,MAAM;IAAE,IAAA,sBAAO,EAAC,IAAI,CAAC,CAAC","sourcesContent":["#!/usr/bin/env node\nimport { execSync, spawnSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport {\n loadAndValidate,\n loadReviewJson,\n prDirFor,\n reviewJsonPath,\n ReviewJson,\n writeTemplate,\n runMain,\n} from '@webpieces/rules-config';\nimport { getFeatureName } from './workflow/git-readAiBranchName';\nimport { baseBranchName } from './workflow/branch-naming';\nimport { assertCleanTree, ensurePushed } from './workflow/git-exec';\nimport { runBuildGate, BuildGateOptions } from './workflow/build-affected';\nimport { mergeDirFor, readMergeMarker } from './workflow/merge-state';\nimport { mergeEnd } from './workflow/merge-end';\nimport { MergeContext } from './workflow/merge-start';\nimport {\n computeGateResults,\n countAddedDisables,\n renderDashboard,\n DashboardInput,\n} from '../dashboard/dashboard';\n\n// FINISH of the AI-first PR flow. Runs after the AI has written review.json (see wp-start-upsert-pr).\n// Responsibilities, in order: (1) if a 3-point merge was in progress, validate + commit + FINALIZE the\n// AI's resolution via merge-END (so the PR is posted from the finalized feature branch, not the squash\n// branch); (2) REQUIRE review.json (hard-fail with the schema if absent/invalid); (3) run the\n// authoritative build gate; (4) render the dashboard; (5) create/update the PR via `gh`. This is the\n// ONLY command that posts PRs.\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\nfunction gitOut(args: string[]): string {\n const result = spawnSync('git', args, { encoding: 'utf8' });\n return result.status === 0 ? (result.stdout ?? '').trim() : '';\n}\n\nfunction buildDashboard(repoRoot: string, buildPassed: boolean, review: ReviewJson): string {\n const config = loadAndValidate(repoRoot).prGate;\n const forkPoint = gitOut(['merge-base', 'origin/main', 'HEAD']);\n const featureHead = gitOut(['rev-parse', 'HEAD']);\n const mainHead = gitOut(['rev-parse', 'origin/main']);\n const range = `${forkPoint}..${featureHead}`;\n const changedFiles = gitOut(['diff', range, '--name-only']).split('\\n').filter((f: string): boolean => f.trim() !== '');\n const patch = gitOut(['diff', range]);\n const title = gitOut(['log', '-1', '--format=%s']);\n\n const gateResults = computeGateResults(config.gates, changedFiles);\n const disables = countAddedDisables(patch);\n const input = new DashboardInput(title, gateResults, disables, buildPassed, forkPoint, featureHead, mainHead, review);\n return renderDashboard(input);\n}\n\n// The PR always lives on the stable base branch (the local branch may be a numbered generation\n// base2/base3/…). Look up / create / merge against `baseBranch`, never the numbered branch, or\n// generation 2+ would fail to find its PR and open a duplicate.\nfunction upsertPr(repoRoot: string, baseBranch: string, body: string): void {\n const prDir = prDirFor(repoRoot, getFeatureName());\n fs.mkdirSync(prDir, { recursive: true });\n const bodyFile = path.join(prDir, 'pr-body.md');\n fs.writeFileSync(bodyFile, body + '\\n');\n\n const existing = gitOut(['log', '-1', '--format=%s']); // title fallback\n const prNumber = spawnSync(\n 'gh', ['pr', 'list', '--head', baseBranch, '--json', 'number', '--jq', '.[0].number'],\n { encoding: 'utf8' },\n );\n const num = prNumber.status === 0 ? (prNumber.stdout ?? '').trim() : '';\n\n if (num === '') {\n process.stdout.write('Creating PR...\\n');\n const create = spawnSync('gh', ['pr', 'create', '--head', baseBranch, '--base', 'main', '--title', existing, '--body-file', bodyFile], { stdio: 'inherit' });\n if (create.status !== 0) {\n process.stderr.write('⚠️ gh pr create failed — create the PR manually with the body in:\\n ' + bodyFile + '\\n');\n return;\n }\n } else {\n process.stdout.write(`Updating PR #${num}...\\n`);\n spawnSync('gh', ['pr', 'edit', num, '--body-file', bodyFile], { stdio: 'inherit' });\n }\n spawnSync('gh', ['pr', 'merge', baseBranch, '--auto', '--squash'], { stdio: 'inherit' });\n}\n\nexport async function main(): Promise<void> {\n const repoRoot = execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim();\n // Refresh the AI-facing workflow doc so it's present + current for any failure message to cite.\n writeTemplate(repoRoot, 'webpieces.git-workflow.md');\n const mergeDir = mergeDirFor(repoRoot, getFeatureName());\n\n // 1. Finish any in-progress conflict resolution: validate + commit + finalize the branch swap.\n // No marker (or already validated) => no merge in progress => nothing to do (the common case).\n const marker = readMergeMarker(mergeDir);\n if (marker && !marker.validated) {\n await mergeEnd(\n repoRoot, 'wp-finish-upsert-pr', mergeDir,\n new MergeContext(marker.currentBranch, marker.squashBranch, marker.backupBranch, marker.prNumber),\n marker.conflictedFiles,\n );\n }\n\n // 2. REQUIRE the AI-authored review.json (throws InformAiError with the schema if missing/invalid).\n const review = loadReviewJson(reviewJsonPath(repoRoot, getFeatureName()));\n\n // 2b. The build gate validates the WORKING TREE but we push HEAD — so they MUST be identical, or a\n // fix edited after the merge commit builds green yet a stale commit gets pushed (CI then fails on\n // the committed tree). Require a clean tree here; the tooling won't commit your work for you.\n assertCleanTree(repoRoot);\n\n // 3. Authoritative build gate, then push, then post.\n runBuildGate(repoRoot, new BuildGateOptions(\n '🛠️ Build gate (authoritative)', 'pnpm wp-finish-upsert-pr', 'Build failed — no PR created/updated.',\n ));\n // After finalize the local branch is the numbered generation (base2/…); the remote branch and\n // PR live on the stable base name — push and upsert against that.\n const base = baseBranchName(execSync('git branch --show-current', { encoding: 'utf8' }).trim());\n ensurePushed(base);\n\n process.stdout.write('\\n' + SEP + '📋 Dashboard + PR\\n' + SEP + '\\n');\n const body = buildDashboard(repoRoot, true, review);\n upsertPr(repoRoot, base, body);\n process.stdout.write('\\n✅ Done.\\n');\n}\n\nif (require.main === module) runMain(main);\n"]}
1
+ {"version":3,"file":"git-finishUpsertPr.js","sourceRoot":"","sources":["../../../../../../packages/tooling/pr-gate/src/scripts/git-finishUpsertPr.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\nimport { execSync, spawnSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport {\n loadAndValidate,\n loadReviewJson,\n prDirFor,\n reviewJsonPath,\n ReviewJson,\n writeTemplate,\n runMain,\n} from '@webpieces/rules-config';\nimport { getFeatureName } from './workflow/git-readAiBranchName';\nimport { baseBranchName } from './workflow/branch-naming';\nimport { assertCleanTree, ensurePushed } from './workflow/git-exec';\nimport { runBuildGate, BuildGateOptions } from './workflow/build-affected';\nimport { mergeDirFor, readMergeMarker, findActiveMergeRunDir } from './workflow/merge-state';\nimport { mergeEnd } from './workflow/merge-end';\nimport { MergeContext } from './workflow/merge-start';\nimport {\n computeGateResults,\n countAddedDisables,\n renderDashboard,\n DashboardInput,\n} from '../dashboard/dashboard';\n\n// FINISH of the AI-first PR flow. Runs after the AI has written review.json (see wp-start-upsert-pr).\n// Responsibilities, in order: (1) if a 3-point merge was in progress, validate + commit + FINALIZE the\n// AI's resolution via merge-END (so the PR is posted from the finalized feature branch, not the squash\n// branch); (2) REQUIRE review.json (hard-fail with the schema if absent/invalid); (3) run the\n// authoritative build gate; (4) render the dashboard; (5) create/update the PR via `gh`. This is the\n// ONLY command that posts PRs.\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\nfunction gitOut(args: string[]): string {\n const result = spawnSync('git', args, { encoding: 'utf8' });\n return result.status === 0 ? (result.stdout ?? '').trim() : '';\n}\n\n// The user-facing PR title: the AI-authored review.title, or — if the AI omitted it — a readable\n// fallback derived from the stable feature name (NEVER the internal `Squash merge of <branch>` commit\n// subject, which leaked bookkeeping into the PR title).\nfunction prTitleFrom(review: ReviewJson): string {\n if (review.title !== '') return review.title;\n return getFeatureName().replace(/[-/]+/g, ' ').trim();\n}\n\nfunction buildDashboard(repoRoot: string, buildPassed: boolean, review: ReviewJson, title: string): string {\n const config = loadAndValidate(repoRoot).prGate;\n const forkPoint = gitOut(['merge-base', 'origin/main', 'HEAD']);\n const featureHead = gitOut(['rev-parse', 'HEAD']);\n const mainHead = gitOut(['rev-parse', 'origin/main']);\n const range = `${forkPoint}..${featureHead}`;\n const changedFiles = gitOut(['diff', range, '--name-only']).split('\\n').filter((f: string): boolean => f.trim() !== '');\n const patch = gitOut(['diff', range]);\n\n const gateResults = computeGateResults(config.gates, changedFiles);\n const disables = countAddedDisables(patch);\n const input = new DashboardInput(title, gateResults, disables, buildPassed, forkPoint, featureHead, mainHead, review);\n return renderDashboard(input);\n}\n\n// The PR, the remote branch, and the local branch all share the one stable feature name now. Look up /\n// create / merge against `baseBranch` (baseBranchName also tolerates a leftover `…wpN` mid-transition),\n// or a resolve from such a leftover could fail to find its PR and open a duplicate.\nfunction upsertPr(repoRoot: string, baseBranch: string, body: string, title: string): string {\n const prDir = prDirFor(repoRoot, getFeatureName());\n fs.mkdirSync(prDir, { recursive: true });\n const bodyFile = path.join(prDir, 'pr-body.md');\n fs.writeFileSync(bodyFile, body + '\\n');\n\n const prNumber = spawnSync(\n 'gh', ['pr', 'list', '--head', baseBranch, '--json', 'number', '--jq', '.[0].number'],\n { encoding: 'utf8' },\n );\n const num = prNumber.status === 0 ? (prNumber.stdout ?? '').trim() : '';\n\n if (num === '') {\n process.stdout.write('Creating PR...\\n');\n const create = spawnSync('gh', ['pr', 'create', '--head', baseBranch, '--base', 'main', '--title', title, '--body-file', bodyFile], { stdio: 'inherit' });\n if (create.status !== 0) {\n process.stderr.write('⚠️ gh pr create failed — create the PR manually with the body in:\\n ' + bodyFile + '\\n');\n return '';\n }\n } else {\n process.stdout.write(`Updating PR #${num}...\\n`);\n // Keep the title in sync with the latest review.title (not just the body).\n spawnSync('gh', ['pr', 'edit', num, '--title', title, '--body-file', bodyFile], { stdio: 'inherit' });\n }\n spawnSync('gh', ['pr', 'merge', baseBranch, '--auto', '--squash'], { stdio: 'inherit' });\n return num;\n}\n\nexport async function main(): Promise<void> {\n const repoRoot = execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim();\n // Refresh the AI-facing workflow doc so it's present + current for any failure message to cite.\n writeTemplate(repoRoot, 'webpieces.git-workflow.md');\n const home = mergeDirFor(repoRoot, getFeatureName());\n\n // 1. Finish any in-progress conflict resolution: validate + commit + finalize the branch swap.\n // No active run dir (or its marker already validated) => no merge in progress => nothing to do\n // (the common case). The active merge is the `merge-<n>/` run dir holding a marker.\n const activeDir = findActiveMergeRunDir(home);\n const marker = activeDir ? readMergeMarker(activeDir) : null;\n if (activeDir && marker && !marker.validated) {\n await mergeEnd(\n repoRoot, 'wp-finish-upsert-pr', activeDir,\n new MergeContext(marker.currentBranch, marker.squashBranch, marker.backupBranch, marker.prNumber),\n marker.conflictedFiles,\n );\n }\n\n // 2. REQUIRE the AI-authored review.json (throws InformAiError with the schema if missing/invalid).\n const review = loadReviewJson(reviewJsonPath(repoRoot, getFeatureName()));\n\n // 2b. The build gate validates the WORKING TREE but we push HEAD — so they MUST be identical, or a\n // fix edited after the merge commit builds green yet a stale commit gets pushed (CI then fails on\n // the committed tree). Require a clean tree here; the tooling won't commit your work for you.\n assertCleanTree(repoRoot);\n\n // 3. Authoritative build gate, then push, then post.\n runBuildGate(repoRoot, new BuildGateOptions(\n '🛠️ Build gate (authoritative)', 'pnpm wp-finish-upsert-pr', 'Build failed — no PR created/updated.',\n ));\n // After finalize the local branch, the remote branch, and the PR all share the SAME stable name —\n // push and upsert against it. (baseBranchName is a no-op on the already-stable name; it also\n // tolerates a leftover `…wpN` mid-transition.)\n const base = baseBranchName(execSync('git branch --show-current', { encoding: 'utf8' }).trim());\n ensurePushed(base);\n\n process.stdout.write('\\n' + SEP + '📋 Dashboard + PR\\n' + SEP + '\\n');\n const title = prTitleFrom(review);\n const body = buildDashboard(repoRoot, true, review, title);\n const prNum = upsertPr(repoRoot, base, body, title);\n\n process.stdout.write(\n '\\n' + SEP + '✅ PR finished — here is exactly what I did\\n' + SEP + '\\n' +\n ` 1. validated the build gate (authoritative)\\n` +\n ` 2. force-pushed your work to origin/${base}\\n` +\n ` 3. ${prNum ? `updated/created PR #${prNum}` : 'created the PR'} titled: \"${title}\"\\n` +\n ` You are on ${base} — same name as the remote branch and the PR head.\\n\\n`,\n );\n}\n\nif (require.main === module) runMain(main);\n"]}
@@ -1,8 +1,12 @@
1
1
  /** Stable base identity (remote / PR / feature-name slug source): trailing `Squash` and the
2
2
  * generation marker stripped. `base` → `base`, `basewp2` → `base`, `basewp2Squash` → `base`. */
3
3
  export declare function baseBranchName(branch: string): string;
4
- /** The local branch you land on after this merge: base + `wp` + (gen + 1). `base` → `basewp2`,
5
- * `basewp2` `basewp3`. */
6
- export declare function nextBranchName(branch: string): string;
7
- /** Pre-merge safety snapshot of the given (current) branch. One overwritable slot per branch. */
8
- export declare function preMergeBackupName(branch: string): string;
4
+ /** Pre-merge snapshot name for slot `n`, ALWAYS numbered from 1: `<branch>PreMerge1`,
5
+ * `<branch>PreMerge2`, … The same `n` also names the paired conflict-context folder
6
+ * (`merge-info/<slug>/merge-<n>/`, see merge-state.mergeRunDirFor), so the branch and its context
7
+ * share one number. Clean syncs delete their snapshot at finalize; only conflict syncs leave one. */
8
+ export declare function preMergeBackupName(branch: string, n: number): string;
9
+ /** First free PreMerge slot NUMBER for `branch`, probing 1, 2, 3, … via the `exists` predicate. Pure
10
+ * (the branch-existence check is injected) so it unit-tests without touching git. This integer is the
11
+ * single source of truth for both the backup branch name and its paired `merge-<n>` context folder. */
12
+ export declare function nextFreePreMergeNumber(branch: string, exists: (name: string) => boolean): number;
@@ -1,23 +1,28 @@
1
1
  "use strict";
2
- // Branch-naming for the numbered-generation squash-merge scheme.
2
+ // Branch-naming for the squash-merge scheme.
3
3
  //
4
- // A feature branch has a stable *base* identity plus a *generation* marker that bumps every time we
5
- // re-sync from main: base basewp2 basewp3 … (gen 1 carries no marker). The remote branch and
6
- // therefore the single PR always lives on `base`; only the LOCAL branch numbers up, so you can see
7
- // at a glance how many times you've re-merged main. The pre-merge safety snapshot is
8
- // `<currentBranch>PreMerge` (one overwritable slot per generation), replacing the old ever-accumulating
9
- // `<branch>Backup1/Backup2/…`.
4
+ // A feature branch has ONE stable name that never changes: it is the local branch, the remote branch,
5
+ // AND the single PR head all identical, so "which branch is my PR on?" is never ambiguous. A sync
6
+ // (re-merge from main) happens on a transient `<branch>Squash` branch and, when it finishes, is
7
+ // force-pushed and RENAMED BACK to the same feature name (see merge-end.finalizeBranch). No `wpN`
8
+ // generation number survives a sync the old scheme numbered the LOCAL branch up (base → basewp2 →
9
+ // basewp3) while the remote/PR stayed on `base`, which read as "the PR moved" when it never did.
10
10
  //
11
- // The generation marker is `wp<N>` (a literal `wp` prefix, NOT a bare number) so it is unambiguous
12
- // against branch names that naturally end in digits most importantly version-upgrade branches like
13
- // `deanhiller/upgrade-webpieces-0.3.213`, which an earlier bare-digit scheme mangled by stripping the
14
- // `213`. Parsing keys off the `wp` marker: strip a trailing `Squash` (the internal temp-branch suffix),
15
- // then a trailing `wp<digits>`. The branch-creation-guard rejects human branches ending in `wp<digits>`
16
- // so the marker stays reserved for this tool.
11
+ // The pre-merge safety snapshot is a NUMBERED trail from 1 `<branch>PreMerge1`, `<branch>PreMerge2`,
12
+ // `<branch>PreMerge3`, Each snapshot is paired 1:1 with a conflict-context folder `merge-<n>/` under
13
+ // the same number (see merge-state). A CLEAN sync deletes its snapshot at finalize (no undo point
14
+ // needed); only CONFLICT syncs leave a snapshot + its `merge-<n>/` behind. See nextFreePreMergeNumber.
15
+ //
16
+ // `baseBranchName` still strips a trailing `Squash` (the transient temp-branch suffix) AND a trailing
17
+ // `wp<digits>` — the latter only for BACKWARD COMPATIBILITY, so a consumer sitting on a leftover
18
+ // `…wp5` branch from the old scheme still resolves to its stable name during the transition. The
19
+ // generation marker `wp<N>` is a literal `wp` prefix (NOT a bare number) so it never mangles branch
20
+ // names that naturally end in digits, e.g. `deanhiller/upgrade-webpieces-0.3.213`. The tool no longer
21
+ // PRODUCES `wpN`; the branch-creation-guard still reserves the suffix during the transition.
17
22
  Object.defineProperty(exports, "__esModule", { value: true });
18
23
  exports.baseBranchName = baseBranchName;
19
- exports.nextBranchName = nextBranchName;
20
24
  exports.preMergeBackupName = preMergeBackupName;
25
+ exports.nextFreePreMergeNumber = nextFreePreMergeNumber;
21
26
  const GENERATION_RE = /^(.*)wp(\d+)$/;
22
27
  class Generation {
23
28
  base;
@@ -40,14 +45,20 @@ function parseGeneration(branch) {
40
45
  function baseBranchName(branch) {
41
46
  return parseGeneration(branch).base;
42
47
  }
43
- /** The local branch you land on after this merge: base + `wp` + (gen + 1). `base` → `basewp2`,
44
- * `basewp2` `basewp3`. */
45
- function nextBranchName(branch) {
46
- const generation = parseGeneration(branch);
47
- return `${generation.base}wp${generation.gen + 1}`;
48
+ /** Pre-merge snapshot name for slot `n`, ALWAYS numbered from 1: `<branch>PreMerge1`,
49
+ * `<branch>PreMerge2`, … The same `n` also names the paired conflict-context folder
50
+ * (`merge-info/<slug>/merge-<n>/`, see merge-state.mergeRunDirFor), so the branch and its context
51
+ * share one number. Clean syncs delete their snapshot at finalize; only conflict syncs leave one. */
52
+ function preMergeBackupName(branch, n) {
53
+ return `${branch}PreMerge${n}`;
48
54
  }
49
- /** Pre-merge safety snapshot of the given (current) branch. One overwritable slot per branch. */
50
- function preMergeBackupName(branch) {
51
- return `${branch}PreMerge`;
55
+ /** First free PreMerge slot NUMBER for `branch`, probing 1, 2, 3, … via the `exists` predicate. Pure
56
+ * (the branch-existence check is injected) so it unit-tests without touching git. This integer is the
57
+ * single source of truth for both the backup branch name and its paired `merge-<n>` context folder. */
58
+ function nextFreePreMergeNumber(branch, exists) {
59
+ for (let n = 1;; n++) {
60
+ if (!exists(preMergeBackupName(branch, n)))
61
+ return n;
62
+ }
52
63
  }
53
64
  //# sourceMappingURL=branch-naming.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"branch-naming.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/branch-naming.ts"],"names":[],"mappings":";AAAA,iEAAiE;AACjE,EAAE;AACF,oGAAoG;AACpG,qGAAqG;AACrG,qGAAqG;AACrG,qFAAqF;AACrF,wGAAwG;AACxG,+BAA+B;AAC/B,EAAE;AACF,mGAAmG;AACnG,qGAAqG;AACrG,sGAAsG;AACtG,wGAAwG;AACxG,wGAAwG;AACxG,8CAA8C;;AAyB9C,wCAEC;AAID,wCAGC;AAGD,gDAEC;AArCD,MAAM,aAAa,GAAG,eAAe,CAAC;AAEtC,MAAM,UAAU;IACZ,IAAI,CAAS;IACb,GAAG,CAAS;IAEZ,YAAY,IAAY,EAAE,GAAW;QACjC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;IACnB,CAAC;CACJ;AAED,SAAS,eAAe,CAAC,MAAc;IACnC,MAAM,aAAa,GAAG,MAAM,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;IACpD,MAAM,KAAK,GAAG,aAAa,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC;IACjD,IAAI,KAAK,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC;QAC3B,OAAO,IAAI,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;IAC5D,CAAC;IACD,OAAO,IAAI,UAAU,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC;AAC5C,CAAC;AAED;iGACiG;AACjG,SAAgB,cAAc,CAAC,MAAc;IACzC,OAAO,eAAe,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC;AACxC,CAAC;AAED;6BAC6B;AAC7B,SAAgB,cAAc,CAAC,MAAc;IACzC,MAAM,UAAU,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC;IAC3C,OAAO,GAAG,UAAU,CAAC,IAAI,KAAK,UAAU,CAAC,GAAG,GAAG,CAAC,EAAE,CAAC;AACvD,CAAC;AAED,iGAAiG;AACjG,SAAgB,kBAAkB,CAAC,MAAc;IAC7C,OAAO,GAAG,MAAM,UAAU,CAAC;AAC/B,CAAC","sourcesContent":["// Branch-naming for the numbered-generation squash-merge scheme.\n//\n// A feature branch has a stable *base* identity plus a *generation* marker that bumps every time we\n// re-sync from main: base basewp2 basewp3 … (gen 1 carries no marker). The remote branch and\n// therefore the single PR always lives on `base`; only the LOCAL branch numbers up, so you can see\n// at a glance how many times you've re-merged main. The pre-merge safety snapshot is\n// `<currentBranch>PreMerge` (one overwritable slot per generation), replacing the old ever-accumulating\n// `<branch>Backup1/Backup2/…`.\n//\n// The generation marker is `wp<N>` (a literal `wp` prefix, NOT a bare number) so it is unambiguous\n// against branch names that naturally end in digits most importantly version-upgrade branches like\n// `deanhiller/upgrade-webpieces-0.3.213`, which an earlier bare-digit scheme mangled by stripping the\n// `213`. Parsing keys off the `wp` marker: strip a trailing `Squash` (the internal temp-branch suffix),\n// then a trailing `wp<digits>`. The branch-creation-guard rejects human branches ending in `wp<digits>`\n// so the marker stays reserved for this tool.\n\nconst GENERATION_RE = /^(.*)wp(\\d+)$/;\n\nclass Generation {\n base: string;\n gen: number;\n\n constructor(base: string, gen: number) {\n this.base = base;\n this.gen = gen;\n }\n}\n\nfunction parseGeneration(branch: string): Generation {\n const withoutSquash = branch.replace(/Squash$/, '');\n const match = withoutSquash.match(GENERATION_RE);\n if (match && match[1] !== '') {\n return new Generation(match[1], parseInt(match[2], 10));\n }\n return new Generation(withoutSquash, 1);\n}\n\n/** Stable base identity (remote / PR / feature-name slug source): trailing `Squash` and the\n * generation marker stripped. `base` → `base`, `basewp2` → `base`, `basewp2Squash` → `base`. */\nexport function baseBranchName(branch: string): string {\n return parseGeneration(branch).base;\n}\n\n/** The local branch you land on after this merge: base + `wp` + (gen + 1). `base` `basewp2`,\n * `basewp2` `basewp3`. */\nexport function nextBranchName(branch: string): string {\n const generation = parseGeneration(branch);\n return `${generation.base}wp${generation.gen + 1}`;\n}\n\n/** Pre-merge safety snapshot of the given (current) branch. One overwritable slot per branch. */\nexport function preMergeBackupName(branch: string): string {\n return `${branch}PreMerge`;\n}\n"]}
1
+ {"version":3,"file":"branch-naming.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/branch-naming.ts"],"names":[],"mappings":";AAAA,6CAA6C;AAC7C,EAAE;AACF,sGAAsG;AACtG,oGAAoG;AACpG,gGAAgG;AAChG,kGAAkG;AAClG,oGAAoG;AACpG,iGAAiG;AACjG,EAAE;AACF,uGAAuG;AACvG,uGAAuG;AACvG,kGAAkG;AAClG,uGAAuG;AACvG,EAAE;AACF,sGAAsG;AACtG,iGAAiG;AACjG,iGAAiG;AACjG,oGAAoG;AACpG,sGAAsG;AACtG,6FAA6F;;AAyB7F,wCAEC;AAMD,gDAEC;AAKD,wDAIC;AA1CD,MAAM,aAAa,GAAG,eAAe,CAAC;AAEtC,MAAM,UAAU;IACZ,IAAI,CAAS;IACb,GAAG,CAAS;IAEZ,YAAY,IAAY,EAAE,GAAW;QACjC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;IACnB,CAAC;CACJ;AAED,SAAS,eAAe,CAAC,MAAc;IACnC,MAAM,aAAa,GAAG,MAAM,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;IACpD,MAAM,KAAK,GAAG,aAAa,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC;IACjD,IAAI,KAAK,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC;QAC3B,OAAO,IAAI,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;IAC5D,CAAC;IACD,OAAO,IAAI,UAAU,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC;AAC5C,CAAC;AAED;iGACiG;AACjG,SAAgB,cAAc,CAAC,MAAc;IACzC,OAAO,eAAe,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC;AACxC,CAAC;AAED;;;sGAGsG;AACtG,SAAgB,kBAAkB,CAAC,MAAc,EAAE,CAAS;IACxD,OAAO,GAAG,MAAM,WAAW,CAAC,EAAE,CAAC;AACnC,CAAC;AAED;;wGAEwG;AACxG,SAAgB,sBAAsB,CAAC,MAAc,EAAE,MAAiC;IACpF,KAAK,IAAI,CAAC,GAAG,CAAC,GAAI,CAAC,EAAE,EAAE,CAAC;QACpB,IAAI,CAAC,MAAM,CAAC,kBAAkB,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;YAAE,OAAO,CAAC,CAAC;IACzD,CAAC;AACL,CAAC","sourcesContent":["// Branch-naming for the squash-merge scheme.\n//\n// A feature branch has ONE stable name that never changes: it is the local branch, the remote branch,\n// AND the single PR head all identical, so \"which branch is my PR on?\" is never ambiguous. A sync\n// (re-merge from main) happens on a transient `<branch>Squash` branch and, when it finishes, is\n// force-pushed and RENAMED BACK to the same feature name (see merge-end.finalizeBranch). No `wpN`\n// generation number survives a sync — the old scheme numbered the LOCAL branch up (base basewp2 →\n// basewp3) while the remote/PR stayed on `base`, which read as \"the PR moved\" when it never did.\n//\n// The pre-merge safety snapshot is a NUMBERED trail from 1 — `<branch>PreMerge1`, `<branch>PreMerge2`,\n// `<branch>PreMerge3`, Each snapshot is paired 1:1 with a conflict-context folder `merge-<n>/` under\n// the same number (see merge-state). A CLEAN sync deletes its snapshot at finalize (no undo point\n// needed); only CONFLICT syncs leave a snapshot + its `merge-<n>/` behind. See nextFreePreMergeNumber.\n//\n// `baseBranchName` still strips a trailing `Squash` (the transient temp-branch suffix) AND a trailing\n// `wp<digits>` the latter only for BACKWARD COMPATIBILITY, so a consumer sitting on a leftover\n// `…wp5` branch from the old scheme still resolves to its stable name during the transition. The\n// generation marker `wp<N>` is a literal `wp` prefix (NOT a bare number) so it never mangles branch\n// names that naturally end in digits, e.g. `deanhiller/upgrade-webpieces-0.3.213`. The tool no longer\n// PRODUCES `wpN`; the branch-creation-guard still reserves the suffix during the transition.\n\nconst GENERATION_RE = /^(.*)wp(\\d+)$/;\n\nclass Generation {\n base: string;\n gen: number;\n\n constructor(base: string, gen: number) {\n this.base = base;\n this.gen = gen;\n }\n}\n\nfunction parseGeneration(branch: string): Generation {\n const withoutSquash = branch.replace(/Squash$/, '');\n const match = withoutSquash.match(GENERATION_RE);\n if (match && match[1] !== '') {\n return new Generation(match[1], parseInt(match[2], 10));\n }\n return new Generation(withoutSquash, 1);\n}\n\n/** Stable base identity (remote / PR / feature-name slug source): trailing `Squash` and the\n * generation marker stripped. `base` → `base`, `basewp2` → `base`, `basewp2Squash` → `base`. */\nexport function baseBranchName(branch: string): string {\n return parseGeneration(branch).base;\n}\n\n/** Pre-merge snapshot name for slot `n`, ALWAYS numbered from 1: `<branch>PreMerge1`,\n * `<branch>PreMerge2`, … The same `n` also names the paired conflict-context folder\n * (`merge-info/<slug>/merge-<n>/`, see merge-state.mergeRunDirFor), so the branch and its context\n * share one number. Clean syncs delete their snapshot at finalize; only conflict syncs leave one. */\nexport function preMergeBackupName(branch: string, n: number): string {\n return `${branch}PreMerge${n}`;\n}\n\n/** First free PreMerge slot NUMBER for `branch`, probing 1, 2, 3, … via the `exists` predicate. Pure\n * (the branch-existence check is injected) so it unit-tests without touching git. This integer is the\n * single source of truth for both the backup branch name and its paired `merge-<n>` context folder. */\nexport function nextFreePreMergeNumber(branch: string, exists: (name: string) => boolean): number {\n for (let n = 1; ; n++) {\n if (!exists(preMergeBackupName(branch, n))) return n;\n }\n}\n"]}
@@ -6,8 +6,8 @@ const child_process_1 = require("child_process");
6
6
  const rules_config_1 = require("@webpieces/rules-config");
7
7
  const branch_naming_1 = require("./branch-naming");
8
8
  // Stable feature identity used to key the merge-context dir and PR-body dir. It MUST stay constant
9
- // across a branch's numbered generations (base base2 → base3) and its transient `Squash` temp, so
10
- // derive it from baseBranchName (strips `Squash` + the generation number) before slugifying.
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
11
  function getFeatureName() {
12
12
  const branch = (0, child_process_1.execSync)('git branch --show-current', { encoding: 'utf8' }).trim();
13
13
  return (0, branch_naming_1.baseBranchName)(branch).replace(/\//g, '-');
@@ -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,oGAAoG;AACpG,6FAA6F;AAC7F,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 branch's numbered generations (base base2 → base3) and its transient `Squash` temp, so\n// derive it from baseBranchName (strips `Squash` + the generation number) 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":";;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"]}
@@ -12,9 +12,9 @@ const git_exec_1 = require("./git-exec");
12
12
  const merge_state_1 = require("./merge-state");
13
13
  // merge-END: the second half of the 3-point squash-merge lifecycle, symmetric with merge-START. Given
14
14
  // the branch context, it (optionally) validates + commits the AI's conflict resolution, then ALWAYS
15
- // finalizes the merge — promotes `<branch>Squash` to the next numbered generation (base base2),
16
- // force-pushes to the stable base branch, stamps a clean main-sync status, clears the marker and
17
- // sweeps stale tmp. The shared runUpdateFromMain (clean path / validated resume), wp-update-end, and
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-update-end, and
18
18
  // wp-finish-upsert-pr (conflict resolution) all call THIS, so finalization happens in exactly one
19
19
  // place and the conflict path can no longer post a PR from the un-swapped squash branch.
20
20
  const SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
@@ -51,13 +51,17 @@ function validateResolution(repoRoot, mergeDir, conflictedFiles) {
51
51
  }
52
52
  process.stdout.write('✅ Merge explanations present for all resolved files.\n');
53
53
  }
54
- // Promote the squash branch to the NEXT numbered generation, force-push to the stable base
55
- // branch (where the single PR lives), and stamp clean main-sync. The local branch numbers up
56
- // (base → base2 → base3) so the generation is visible; the remote/PR name stays `base`.
57
- function finalizeBranch(repoRoot, verb, ctx) {
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) {
58
63
  process.stdout.write('\n' + SEP + '🗑️ Finalizing\n' + SEP + '\n');
59
64
  const base = (0, branch_naming_1.baseBranchName)(ctx.currentBranch);
60
- const next = (0, branch_naming_1.nextBranchName)(ctx.currentBranch);
61
65
  (0, git_exec_1.runGitChecked)(['branch', '-D', ctx.currentBranch], 'Failed to delete old feature branch');
62
66
  const remoteExists = (0, child_process_1.spawnSync)('git', ['ls-remote', '--exit-code', '--heads', 'origin', base]).status === 0;
63
67
  if (remoteExists) {
@@ -68,22 +72,54 @@ function finalizeBranch(repoRoot, verb, ctx) {
68
72
  process.stdout.write('No remote branch — local only.\n');
69
73
  }
70
74
  (0, git_exec_1.runGitChecked)(['checkout', ctx.squashBranch], 'Failed to checkout squash branch');
71
- (0, git_exec_1.runGitChecked)(['branch', '-m', next], 'Failed to rename 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');
79
+ }
80
+ (0, git_exec_1.runGitChecked)(['branch', '-m', base], 'Failed to rename squash branch to the feature name');
72
81
  const renameEvent = new rules_config_1.BranchMutationEvent(verb, 'RENAME');
73
82
  renameEvent.fromBranch = ctx.currentBranch;
74
- renameEvent.toBranch = next;
83
+ renameEvent.toBranch = base;
75
84
  (0, rules_config_1.logBranchMutation)(repoRoot, renameEvent);
76
85
  // Branch now contains origin/main — stamp a clean main-sync status so the feature-branch-guard
77
86
  // unblocks edits immediately (no wait for the async refresher).
78
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');
93
+ }
79
94
  const finalizeEvent = new rules_config_1.BranchMutationEvent(verb, 'FINALIZE');
80
95
  finalizeEvent.fromBranch = ctx.currentBranch;
81
- finalizeEvent.toBranch = next;
96
+ finalizeEvent.toBranch = base;
82
97
  finalizeEvent.outcome = 'finalized';
83
- finalizeEvent.artifacts = [`backup=${ctx.backupBranch}`, `remotePR=${base}`];
98
+ finalizeEvent.artifacts = [backupKept ? `backup=${ctx.backupBranch}` : `backupDeleted=${ctx.backupBranch}`, `remotePR=${base}`];
84
99
  (0, rules_config_1.logBranchMutation)(repoRoot, finalizeEvent);
85
- process.stdout.write(`\n✅ Now on ${next} (remote/PR: ${base}), updated from main. Backup: ${ctx.backupBranch}\n`);
86
- process.stdout.write(` Delete backup when safe: git branch -D ${ctx.backupBranch}\n\n`);
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);
87
123
  }
88
124
  /**
89
125
  * Complete a 3-point squash merge. `conflictedFiles` non-null means a conflict was resolved by the AI
@@ -106,7 +142,11 @@ async function mergeEnd(repoRoot, verb, mergeDir, ctx, conflictedFiles) {
106
142
  fs.writeFileSync(path.join(mergeDir, 'conflicts-resolved'), '');
107
143
  process.stdout.write('\n✅ Merge validated and committed.\n');
108
144
  }
109
- finalizeBranch(repoRoot, verb, ctx);
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);
110
150
  (0, merge_state_1.clearMergeMarker)(mergeDir);
111
151
  await (0, cleanTmp_1.cleanTmp)();
112
152
  }
@@ -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, nextBranchName } from './branch-naming';\nimport { cleanTmp } from './cleanTmp';\nimport { assertNoUntracked, runGitChecked } from './git-exec';\nimport { MergeContext } from './merge-start';\nimport { clearMergeMarker, 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 — promotes `<branch>Squash` to the next numbered generation (base → base2),\n// force-pushes to the stable base branch, stamps a clean main-sync status, clears the marker and\n// sweeps stale tmp. The shared runUpdateFromMain (clean path / validated resume), wp-update-end, 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\n// Promote the squash branch to the NEXT numbered generation, force-push to the stable base\n// branch (where the single PR lives), and stamp clean main-sync. The local branch numbers up\n// (base → base2 → base3) so the generation is visible; the remote/PR name stays `base`.\nfunction finalizeBranch(repoRoot: string, verb: MutationVerb, ctx: MergeContext): void {\n process.stdout.write('\\n' + SEP + '🗑️ Finalizing\\n' + SEP + '\\n');\n const base = baseBranchName(ctx.currentBranch);\n const next = nextBranchName(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 runGitChecked(['branch', '-m', next], 'Failed to rename squash branch');\n const renameEvent = new BranchMutationEvent(verb, 'RENAME');\n renameEvent.fromBranch = ctx.currentBranch;\n renameEvent.toBranch = next;\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 const finalizeEvent = new BranchMutationEvent(verb, 'FINALIZE');\n finalizeEvent.fromBranch = ctx.currentBranch;\n finalizeEvent.toBranch = next;\n finalizeEvent.outcome = 'finalized';\n finalizeEvent.artifacts = [`backup=${ctx.backupBranch}`, `remotePR=${base}`];\n logBranchMutation(repoRoot, finalizeEvent);\n\n process.stdout.write(`\\n✅ Now on ${next} (remote/PR: ${base}), updated from main. Backup: ${ctx.backupBranch}\\n`);\n process.stdout.write(` Delete backup when safe: git branch -D ${ctx.backupBranch}\\n\\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 finalizeBranch(repoRoot, verb, ctx);\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 { 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-update-end, 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"]}
@@ -9,6 +9,7 @@ export declare class MergeContext {
9
9
  export declare class MergeStartResult {
10
10
  status: 'clean' | 'conflict';
11
11
  context: MergeContext | null;
12
- constructor(status: 'clean' | 'conflict', context: MergeContext | null);
12
+ runDir: string;
13
+ constructor(status: 'clean' | 'conflict', context: MergeContext | null, runDir: string);
13
14
  }
14
- export declare function mergeStart(repoRoot: string, verb: MutationVerb, mergeDir: string, finishCommand: string): Promise<MergeStartResult>;
15
+ export declare function mergeStart(repoRoot: string, verb: MutationVerb, home: string, finishCommand: string): Promise<MergeStartResult>;