@webpieces/pr-gate 0.4.677 → 0.4.679

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.4.677",
3
+ "version": "0.4.679",
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",
@@ -15,7 +15,7 @@
15
15
  "directory": "packages/tooling/pr-gate"
16
16
  },
17
17
  "dependencies": {
18
- "@webpieces/rules-config": "0.4.677",
18
+ "@webpieces/rules-config": "0.4.679",
19
19
  "@inversifyjs/binding-decorators": "1.1.5",
20
20
  "inversify": "7.10.4",
21
21
  "reflect-metadata": "0.2.2"
@@ -1,5 +1,6 @@
1
1
  import { OrphanDirSweeper, RepoRootFinder } from '@webpieces/rules-config';
2
2
  import { CleanupCommand } from './cleanup-command';
3
+ import { WorkingTreeGate } from './working-tree-gate';
3
4
  /**
4
5
  * `wp-checkout-clean-main` — go to main, get main, and take out the trash. ONE command, because the three
5
6
  * are one intention and splitting them is what let the third never happen.
@@ -44,7 +45,8 @@ export declare class CheckoutCleanMainCommand {
44
45
  private readonly repoRootFinder;
45
46
  private readonly cleanupCommand;
46
47
  private readonly orphanDirSweeper;
47
- constructor(repoRootFinder: RepoRootFinder, cleanupCommand: CleanupCommand, orphanDirSweeper: OrphanDirSweeper);
48
+ private readonly workingTreeGate;
49
+ constructor(repoRootFinder: RepoRootFinder, cleanupCommand: CleanupCommand, orphanDirSweeper: OrphanDirSweeper, workingTreeGate: WorkingTreeGate);
48
50
  run(): Promise<void>;
49
51
  /**
50
52
  * A linked worktree has no main to check out — `git checkout main` FATALS there with "main is already
@@ -55,10 +57,11 @@ export declare class CheckoutCleanMainCommand {
55
57
  private sweepOnly;
56
58
  private isLinkedWorktree;
57
59
  /**
58
- * Refuse on a dirty tree instead of carrying the changes onto main. `git checkout` would happily
59
- * bring uncommitted work along, and the one place nobody wants to discover their work is on main.
60
+ * Say which untracked files were let through, AFTER the move so nobody has to wonder whether the
61
+ * files still sitting in their tree came along for the ride. Printed rather than refused on: see
62
+ * `WorkingTreeGate` for why an untracked file is not the hazard a tracked one is.
60
63
  */
61
- private assertTreeIsClean;
64
+ private reportUntracked;
62
65
  /**
63
66
  * Checkout main and fast-forward it. `--ff-only` rather than a `reset --hard`, deliberately: a
64
67
  * developer or an agent that accidentally committed to local main would have that work silently
@@ -71,8 +74,6 @@ export declare class CheckoutCleanMainCommand {
71
74
  * permitted to turn somebody's completed sync into a failed command.
72
75
  */
73
76
  private sweep;
74
- /** Captured git output, or null when git could not be run or exited non-zero. */
75
- private git;
76
77
  /** git with its output going straight to the terminal — for the commands whose progress is the point. */
77
78
  private run_;
78
79
  }
@@ -6,6 +6,7 @@ const child_process_1 = require("child_process");
6
6
  const inversify_1 = require("inversify");
7
7
  const rules_config_1 = require("@webpieces/rules-config");
8
8
  const cleanup_command_1 = require("./cleanup-command");
9
+ const working_tree_gate_1 = require("./working-tree-gate");
9
10
  const SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
10
11
  /**
11
12
  * `wp-checkout-clean-main` — go to main, get main, and take out the trash. ONE command, because the three
@@ -51,10 +52,12 @@ let CheckoutCleanMainCommand = class CheckoutCleanMainCommand {
51
52
  repoRootFinder;
52
53
  cleanupCommand;
53
54
  orphanDirSweeper;
54
- constructor(repoRootFinder, cleanupCommand, orphanDirSweeper) {
55
+ workingTreeGate;
56
+ constructor(repoRootFinder, cleanupCommand, orphanDirSweeper, workingTreeGate) {
55
57
  this.repoRootFinder = repoRootFinder;
56
58
  this.cleanupCommand = cleanupCommand;
57
59
  this.orphanDirSweeper = orphanDirSweeper;
60
+ this.workingTreeGate = workingTreeGate;
58
61
  }
59
62
  async run() {
60
63
  const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());
@@ -63,8 +66,9 @@ let CheckoutCleanMainCommand = class CheckoutCleanMainCommand {
63
66
  await this.sweepOnly(repoRoot);
64
67
  return;
65
68
  }
66
- this.assertTreeIsClean(repoRoot);
69
+ const untracked = this.workingTreeGate.assertNoTrackedChanges(repoRoot);
67
70
  this.goToMain(repoRoot);
71
+ this.reportUntracked(untracked);
68
72
  await this.cleanupCommand.run();
69
73
  this.sweep(repoRoot);
70
74
  }
@@ -88,17 +92,15 @@ let CheckoutCleanMainCommand = class CheckoutCleanMainCommand {
88
92
  return dirs !== null && dirs.isLinkedWorktree;
89
93
  }
90
94
  /**
91
- * Refuse on a dirty tree instead of carrying the changes onto main. `git checkout` would happily
92
- * bring uncommitted work along, and the one place nobody wants to discover their work is on main.
95
+ * Say which untracked files were let through, AFTER the move so nobody has to wonder whether the
96
+ * files still sitting in their tree came along for the ride. Printed rather than refused on: see
97
+ * `WorkingTreeGate` for why an untracked file is not the hazard a tracked one is.
93
98
  */
94
- assertTreeIsClean(repoRoot) {
95
- const status = this.git(repoRoot, ['status', '--porcelain']);
96
- if (status === null || status.trim() === '')
99
+ reportUntracked(untracked) {
100
+ const rendered = untracked.render();
101
+ if (rendered === '')
97
102
  return;
98
- throw new rules_config_1.CliExitError(1, `${SEP}❌ Uncommitted or untracked changes\n${SEP}\n`
99
- + 'Going to main would carry them along. Commit them on this branch, or stash them, then\n'
100
- + 're-run. The webpieces tooling never commits your work for you.\n\n'
101
- + `${status}`);
103
+ process.stdout.write(`\n${rendered}`);
102
104
  }
103
105
  /**
104
106
  * Checkout main and fast-forward it. `--ff-only` rather than a `reset --hard`, deliberately: a
@@ -130,15 +132,6 @@ let CheckoutCleanMainCommand = class CheckoutCleanMainCommand {
130
132
  return;
131
133
  process.stdout.write(`\n${rendered}`);
132
134
  }
133
- /** Captured git output, or null when git could not be run or exited non-zero. */
134
- git(repoRoot, args) {
135
- const result = (0, child_process_1.spawnSync)('git', ['-C', repoRoot, ...args], {
136
- encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
137
- });
138
- if (result.error !== undefined || result.status !== 0)
139
- return null;
140
- return result.stdout;
141
- }
142
135
  /** git with its output going straight to the terminal — for the commands whose progress is the point. */
143
136
  run_(repoRoot, args) {
144
137
  const result = (0, child_process_1.spawnSync)('git', ['-C', repoRoot, ...args], { encoding: 'utf8', stdio: 'inherit' });
@@ -150,6 +143,7 @@ exports.CheckoutCleanMainCommand = CheckoutCleanMainCommand = tslib_1.__decorate
150
143
  (0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton),
151
144
  tslib_1.__metadata("design:paramtypes", [rules_config_1.RepoRootFinder,
152
145
  cleanup_command_1.CleanupCommand,
153
- rules_config_1.OrphanDirSweeper])
146
+ rules_config_1.OrphanDirSweeper,
147
+ working_tree_gate_1.WorkingTreeGate])
154
148
  ], CheckoutCleanMainCommand);
155
149
  //# sourceMappingURL=checkout-clean-main-command.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"checkout-clean-main-command.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/checkout-clean-main-command.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from 'child_process';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport {\n CliExitError,\n OrphanDirSweeper,\n RepoRootFinder,\n dotWebpieces,\n} from '@webpieces/rules-config';\n\nimport { CleanupCommand } from './cleanup-command';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * `wp-checkout-clean-main` — go to main, get main, and take out the trash. ONE command, because the three\n * are one intention and splitting them is what let the third never happen.\n *\n * ─── ITS RELATIONSHIP TO `git checkout main && git pull origin main` (READ BEFORE \"DELETING\" THAT) ────\n * That pair was already the enforced spelling: bare `git checkout main` is blocked precisely so the pull\n * rides along. This command is the same pairing with cleanup and the sweep welded on, and the WORKFLOW\n * messages now prescribe this instead — two spellings where one sweeps and one does not is shim shape\n * #1, an agent types whichever is accepted and the corpses accumulate forever.\n *\n * The raw pair is NOT deleted, and the reason is a constraint that outranks the shim rule: it is a\n * TERMINAL ENTRY ON THE L0 ALLOWLIST (`CHECKOUT_MAIN_PULL_CMD` in ai-hook-rules\n * `src/bin/l0-allowlist.ts`, consumed as an L0 cure in `src/core/l0-matrix.ts`) — one of\n * the few commands still permitted while an L0 block denies every other tool call. In exactly that\n * state, `node_modules` is the thing that is untrustworthy, so a `pnpm wp-*` bin is the one kind of cure\n * that cannot be relied on to run. The same reasoning covers the L0 shim's own fix option in\n * `templates/ai-hook.sh` (rendered from `src/bin/shim-drift-fix.ts`).\n *\n * So the end state is TWO LAYERS, not a deletion, and it is now in place:\n * • L0 recovery (l0-allowlist, shim-drift-fix, l0-matrix) → keeps the raw pair. It is not a shim\n * there; it is the only thing that works when the package manager's output is what is in doubt.\n * • the WORKFLOW layer (stale-main-bash-guard's preferred cure, merged-branch-message/-bash-guard,\n * TreeRecovery's cleanupSteps and updateMainSteps, the L2 rows/doc, CLAUDE.md's \"Finishing a\n * Feature\") → prescribes `pnpm wp-checkout-clean-main`, and no longer prints the pair.\n *\n * The pair remains ALLOWED as a Bash command in both layers — this changed what the guards TEACH, not\n * what they permit. Blocking it would have deleted the L0 escape along with the shim.\n *\n * ─── WHY GOING TO MAIN IS THE RIGHT MOMENT TO SWEEP ───────────────────────────────────────────────────\n * The corpses appear when git moves the working tree onto a base where the tracked files under some\n * directory are gone. Landing a PR and returning to main is when every developer does that, on every\n * clone, roughly once per merged PR — so a sweep bolted here converges the whole team's machines with no\n * new trigger, no git hook to distribute, and no background daemon. It is also the quietest moment in the\n * day to move directories: nothing is building, no dev server is watching files.\n *\n * ─── WHY THE ORDER IS FIXED AND NOT PARALLEL ──────────────────────────────────────────────────────────\n * Cleanup runs BEFORE the sweep and not beside it. `wp-cleanup` removes dead worktrees, which changes\n * what is on disk, and a scan racing that would be reading a tree mid-demolition. The scan is one\n * ignore-walk and costs about a second; there is no wall-clock worth buying with that race.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class CheckoutCleanMainCommand {\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly cleanupCommand: CleanupCommand,\n private readonly orphanDirSweeper: OrphanDirSweeper,\n ) {}\n\n async run(): Promise<void> {\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n process.stdout.write(`${SEP}🧹 Checkout main, pull, clean\\n${SEP}\\n`);\n if (this.isLinkedWorktree(repoRoot)) {\n await this.sweepOnly(repoRoot);\n return;\n }\n this.assertTreeIsClean(repoRoot);\n this.goToMain(repoRoot);\n await this.cleanupCommand.run();\n this.sweep(repoRoot);\n }\n\n /**\n * A linked worktree has no main to check out — `git checkout main` FATALS there with \"main is already\n * checked out at <primary clone>\", which is a footgun CLAUDE.md currently has to warn about in prose.\n * So the command does the half that IS meaningful here (worktrees accumulate corpses too, if a\n * long-lived one keeps merging main in) and says plainly where the other half lives.\n */\n private async sweepOnly(repoRoot: string): Promise<void> {\n process.stdout.write(\n 'This is a linked WORKTREE. main is checked out in the primary clone, so there is nothing\\n'\n + 'here to check out or pull — sweeping this tree only.\\n\\n'\n + 'Most worktrees never need this: `wp-cleanup` removes the whole directory once its branch\\n'\n + 'lands, which takes any orphan directories inside it along. Run this command again from the\\n'\n + 'primary clone to go to main there.\\n\\n');\n this.sweep(repoRoot);\n return Promise.resolve();\n }\n\n private isLinkedWorktree(repoRoot: string): boolean {\n const dirs = dotWebpieces.gitDirs(repoRoot);\n return dirs !== null && dirs.isLinkedWorktree;\n }\n\n /**\n * Refuse on a dirty tree instead of carrying the changes onto main. `git checkout` would happily\n * bring uncommitted work along, and the one place nobody wants to discover their work is on main.\n */\n private assertTreeIsClean(repoRoot: string): void {\n const status = this.git(repoRoot, ['status', '--porcelain']);\n if (status === null || status.trim() === '') return;\n throw new CliExitError(1,\n `${SEP}❌ Uncommitted or untracked changes\\n${SEP}\\n`\n + 'Going to main would carry them along. Commit them on this branch, or stash them, then\\n'\n + 're-run. The webpieces tooling never commits your work for you.\\n\\n'\n + `${status}`);\n }\n\n /**\n * Checkout main and fast-forward it. `--ff-only` rather than a `reset --hard`, deliberately: a\n * developer or an agent that accidentally committed to local main would have that work silently\n * destroyed by a reset, and the whole point of this command is that it is safe to run without\n * thinking. A refusal to fast-forward is a real condition a human should see and decide about.\n */\n private goToMain(repoRoot: string): void {\n this.run_(repoRoot, ['fetch', 'origin', 'main']);\n if (this.run_(repoRoot, ['checkout', 'main']) !== 0) {\n throw new CliExitError(1, `${SEP}❌ Could not check out main\\n${SEP}\\n`\n + 'git refused the checkout — its message is above.\\n');\n }\n if (this.run_(repoRoot, ['pull', '--ff-only', 'origin', 'main']) !== 0) {\n throw new CliExitError(1, `${SEP}❌ Local main could not fast-forward\\n${SEP}\\n`\n + 'Local main has commits that origin/main does not, so it is not a clean copy of the\\n'\n + 'remote. Nothing was reset — those commits may be the only copy. Inspect them with\\n'\n + '`git log origin/main..main` and decide what they are before going further.\\n');\n }\n }\n\n /**\n * Sweep, then print. Never throws: the checkout and pull above already succeeded, and a tidier is not\n * permitted to turn somebody's completed sync into a failed command.\n */\n private sweep(repoRoot: string): void {\n const report = this.orphanDirSweeper.sweep(repoRoot, new Date());\n const rendered = report.render();\n if (rendered === '') return;\n process.stdout.write(`\\n${rendered}`);\n }\n\n /** Captured git output, or null when git could not be run or exited non-zero. */\n private git(repoRoot: string, args: string[]): string | null {\n const result = spawnSync('git', ['-C', repoRoot, ...args], {\n encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],\n });\n if (result.error !== undefined || result.status !== 0) return null;\n return result.stdout;\n }\n\n /** git with its output going straight to the terminal — for the commands whose progress is the point. */\n private run_(repoRoot: string, args: string[]): number {\n const result = spawnSync('git', ['-C', repoRoot, ...args], { encoding: 'utf8', stdio: 'inherit' });\n return result.status === null ? 1 : result.status;\n }\n}\n"]}
1
+ {"version":3,"file":"checkout-clean-main-command.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/checkout-clean-main-command.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from 'child_process';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport {\n CliExitError,\n OrphanDirSweeper,\n RepoRootFinder,\n dotWebpieces,\n} from '@webpieces/rules-config';\n\nimport { CleanupCommand } from './cleanup-command';\nimport { WorkingTreeGate, UntrackedFiles } from './working-tree-gate';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * `wp-checkout-clean-main` — go to main, get main, and take out the trash. ONE command, because the three\n * are one intention and splitting them is what let the third never happen.\n *\n * ─── ITS RELATIONSHIP TO `git checkout main && git pull origin main` (READ BEFORE \"DELETING\" THAT) ────\n * That pair was already the enforced spelling: bare `git checkout main` is blocked precisely so the pull\n * rides along. This command is the same pairing with cleanup and the sweep welded on, and the WORKFLOW\n * messages now prescribe this instead — two spellings where one sweeps and one does not is shim shape\n * #1, an agent types whichever is accepted and the corpses accumulate forever.\n *\n * The raw pair is NOT deleted, and the reason is a constraint that outranks the shim rule: it is a\n * TERMINAL ENTRY ON THE L0 ALLOWLIST (`CHECKOUT_MAIN_PULL_CMD` in ai-hook-rules\n * `src/bin/l0-allowlist.ts`, consumed as an L0 cure in `src/core/l0-matrix.ts`) — one of\n * the few commands still permitted while an L0 block denies every other tool call. In exactly that\n * state, `node_modules` is the thing that is untrustworthy, so a `pnpm wp-*` bin is the one kind of cure\n * that cannot be relied on to run. The same reasoning covers the L0 shim's own fix option in\n * `templates/ai-hook.sh` (rendered from `src/bin/shim-drift-fix.ts`).\n *\n * So the end state is TWO LAYERS, not a deletion, and it is now in place:\n * • L0 recovery (l0-allowlist, shim-drift-fix, l0-matrix) → keeps the raw pair. It is not a shim\n * there; it is the only thing that works when the package manager's output is what is in doubt.\n * • the WORKFLOW layer (stale-main-bash-guard's preferred cure, merged-branch-message/-bash-guard,\n * TreeRecovery's cleanupSteps and updateMainSteps, the L2 rows/doc, CLAUDE.md's \"Finishing a\n * Feature\") → prescribes `pnpm wp-checkout-clean-main`, and no longer prints the pair.\n *\n * The pair remains ALLOWED as a Bash command in both layers — this changed what the guards TEACH, not\n * what they permit. Blocking it would have deleted the L0 escape along with the shim.\n *\n * ─── WHY GOING TO MAIN IS THE RIGHT MOMENT TO SWEEP ───────────────────────────────────────────────────\n * The corpses appear when git moves the working tree onto a base where the tracked files under some\n * directory are gone. Landing a PR and returning to main is when every developer does that, on every\n * clone, roughly once per merged PR — so a sweep bolted here converges the whole team's machines with no\n * new trigger, no git hook to distribute, and no background daemon. It is also the quietest moment in the\n * day to move directories: nothing is building, no dev server is watching files.\n *\n * ─── WHY THE ORDER IS FIXED AND NOT PARALLEL ──────────────────────────────────────────────────────────\n * Cleanup runs BEFORE the sweep and not beside it. `wp-cleanup` removes dead worktrees, which changes\n * what is on disk, and a scan racing that would be reading a tree mid-demolition. The scan is one\n * ignore-walk and costs about a second; there is no wall-clock worth buying with that race.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class CheckoutCleanMainCommand {\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly cleanupCommand: CleanupCommand,\n private readonly orphanDirSweeper: OrphanDirSweeper,\n private readonly workingTreeGate: WorkingTreeGate,\n ) {}\n\n async run(): Promise<void> {\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n process.stdout.write(`${SEP}🧹 Checkout main, pull, clean\\n${SEP}\\n`);\n if (this.isLinkedWorktree(repoRoot)) {\n await this.sweepOnly(repoRoot);\n return;\n }\n const untracked = this.workingTreeGate.assertNoTrackedChanges(repoRoot);\n this.goToMain(repoRoot);\n this.reportUntracked(untracked);\n await this.cleanupCommand.run();\n this.sweep(repoRoot);\n }\n\n /**\n * A linked worktree has no main to check out — `git checkout main` FATALS there with \"main is already\n * checked out at <primary clone>\", which is a footgun CLAUDE.md currently has to warn about in prose.\n * So the command does the half that IS meaningful here (worktrees accumulate corpses too, if a\n * long-lived one keeps merging main in) and says plainly where the other half lives.\n */\n private async sweepOnly(repoRoot: string): Promise<void> {\n process.stdout.write(\n 'This is a linked WORKTREE. main is checked out in the primary clone, so there is nothing\\n'\n + 'here to check out or pull — sweeping this tree only.\\n\\n'\n + 'Most worktrees never need this: `wp-cleanup` removes the whole directory once its branch\\n'\n + 'lands, which takes any orphan directories inside it along. Run this command again from the\\n'\n + 'primary clone to go to main there.\\n\\n');\n this.sweep(repoRoot);\n return Promise.resolve();\n }\n\n private isLinkedWorktree(repoRoot: string): boolean {\n const dirs = dotWebpieces.gitDirs(repoRoot);\n return dirs !== null && dirs.isLinkedWorktree;\n }\n\n /**\n * Say which untracked files were let through, AFTER the move — so nobody has to wonder whether the\n * files still sitting in their tree came along for the ride. Printed rather than refused on: see\n * `WorkingTreeGate` for why an untracked file is not the hazard a tracked one is.\n */\n private reportUntracked(untracked: UntrackedFiles): void {\n const rendered = untracked.render();\n if (rendered === '') return;\n process.stdout.write(`\\n${rendered}`);\n }\n\n /**\n * Checkout main and fast-forward it. `--ff-only` rather than a `reset --hard`, deliberately: a\n * developer or an agent that accidentally committed to local main would have that work silently\n * destroyed by a reset, and the whole point of this command is that it is safe to run without\n * thinking. A refusal to fast-forward is a real condition a human should see and decide about.\n */\n private goToMain(repoRoot: string): void {\n this.run_(repoRoot, ['fetch', 'origin', 'main']);\n if (this.run_(repoRoot, ['checkout', 'main']) !== 0) {\n throw new CliExitError(1, `${SEP}❌ Could not check out main\\n${SEP}\\n`\n + 'git refused the checkout — its message is above.\\n');\n }\n if (this.run_(repoRoot, ['pull', '--ff-only', 'origin', 'main']) !== 0) {\n throw new CliExitError(1, `${SEP}❌ Local main could not fast-forward\\n${SEP}\\n`\n + 'Local main has commits that origin/main does not, so it is not a clean copy of the\\n'\n + 'remote. Nothing was reset — those commits may be the only copy. Inspect them with\\n'\n + '`git log origin/main..main` and decide what they are before going further.\\n');\n }\n }\n\n /**\n * Sweep, then print. Never throws: the checkout and pull above already succeeded, and a tidier is not\n * permitted to turn somebody's completed sync into a failed command.\n */\n private sweep(repoRoot: string): void {\n const report = this.orphanDirSweeper.sweep(repoRoot, new Date());\n const rendered = report.render();\n if (rendered === '') return;\n process.stdout.write(`\\n${rendered}`);\n }\n\n /** git with its output going straight to the terminal — for the commands whose progress is the point. */\n private run_(repoRoot: string, args: string[]): number {\n const result = spawnSync('git', ['-C', repoRoot, ...args], { encoding: 'utf8', stdio: 'inherit' });\n return result.status === null ? 1 : result.status;\n }\n}\n"]}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * The untracked files a branch switch is about to leave exactly where they are — carried out of the gate
3
+ * so the caller can SAY so, rather than surprise anyone with files that appear to have followed them.
4
+ */
5
+ export declare class UntrackedFiles {
6
+ readonly paths: readonly string[];
7
+ constructor(paths: readonly string[]);
8
+ isEmpty(): boolean;
9
+ /** The human-facing block, or '' when there is nothing to say. */
10
+ render(): string;
11
+ }
12
+ /**
13
+ * The clean-tree gate for `wp-checkout-clean-main`: refuse when TRACKED work would ride onto main, and
14
+ * only then.
15
+ *
16
+ * ─── WHY UNTRACKED FILES ARE NOT A REASON TO REFUSE ───────────────────────────────────────────────────
17
+ * The hazard the gate exists for is real but narrow: `git checkout` carries uncommitted changes to
18
+ * TRACKED files across the switch, and main is the one place nobody wants to discover their work. An
19
+ * untracked file has none of that. It is not on any branch, the switch does not move it, and no branch
20
+ * can end up holding it — so refusing on one blocks a routine, safe operation.
21
+ *
22
+ * Worse, it was a refusal the user could not clear without deleting their file. The message prescribed
23
+ * "commit them on this branch, or stash them", and a plain `git stash` does NOT take untracked files
24
+ * (that needs `-u`) — so a user who did exactly what the error said got the identical refusal back. That
25
+ * is the shape this repo treats as a defect in its own right: a cure that does not resolve the state it
26
+ * is prescribed for. Reported from the field, where a generated `design.html` sitting untracked made the
27
+ * command permanently unusable in that clone.
28
+ *
29
+ * Now the refusal only ever names tracked changes, so `git stash` — the cure it prints — always resolves
30
+ * it, by construction.
31
+ *
32
+ * ─── WHY `--untracked-files=no` AND `ls-files -z` RATHER THAN PARSING `??` ─────────────────────────────
33
+ * The decision half asks git the tracked-only question directly, so no line of porcelain output is ever
34
+ * parsed to make it. The reporting half asks `ls-files --others --exclude-standard -z`, which emits bare
35
+ * NUL-separated paths — no `?? ` prefix to strip and, crucially, no quoting: porcelain v1 wraps paths
36
+ * containing special characters in quotes with C-style escapes, so a hand-parser would hand back a
37
+ * mangled name for exactly the paths a human most needs to read correctly.
38
+ */
39
+ export declare class WorkingTreeGate {
40
+ /**
41
+ * Throw when tracked files are modified or staged; otherwise hand back the untracked files that were
42
+ * deliberately allowed through, for the caller to mention.
43
+ */
44
+ assertNoTrackedChanges(repoRoot: string): UntrackedFiles;
45
+ private untrackedFiles;
46
+ /** Captured git output, or null when git could not be run or exited non-zero. */
47
+ private git;
48
+ }
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WorkingTreeGate = exports.UntrackedFiles = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const child_process_1 = require("child_process");
6
+ const inversify_1 = require("inversify");
7
+ const rules_config_1 = require("@webpieces/rules-config");
8
+ const SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
9
+ /**
10
+ * The untracked files a branch switch is about to leave exactly where they are — carried out of the gate
11
+ * so the caller can SAY so, rather than surprise anyone with files that appear to have followed them.
12
+ */
13
+ class UntrackedFiles {
14
+ paths;
15
+ constructor(paths) {
16
+ this.paths = paths;
17
+ }
18
+ isEmpty() {
19
+ return this.paths.length === 0;
20
+ }
21
+ /** The human-facing block, or '' when there is nothing to say. */
22
+ render() {
23
+ if (this.isEmpty())
24
+ return '';
25
+ const list = this.paths.map((path) => ` ${path}\n`).join('');
26
+ return 'These untracked files are not on any branch, so the checkout leaves them exactly where\n'
27
+ + 'they are — nothing about them changed:\n'
28
+ + list;
29
+ }
30
+ }
31
+ exports.UntrackedFiles = UntrackedFiles;
32
+ /**
33
+ * The clean-tree gate for `wp-checkout-clean-main`: refuse when TRACKED work would ride onto main, and
34
+ * only then.
35
+ *
36
+ * ─── WHY UNTRACKED FILES ARE NOT A REASON TO REFUSE ───────────────────────────────────────────────────
37
+ * The hazard the gate exists for is real but narrow: `git checkout` carries uncommitted changes to
38
+ * TRACKED files across the switch, and main is the one place nobody wants to discover their work. An
39
+ * untracked file has none of that. It is not on any branch, the switch does not move it, and no branch
40
+ * can end up holding it — so refusing on one blocks a routine, safe operation.
41
+ *
42
+ * Worse, it was a refusal the user could not clear without deleting their file. The message prescribed
43
+ * "commit them on this branch, or stash them", and a plain `git stash` does NOT take untracked files
44
+ * (that needs `-u`) — so a user who did exactly what the error said got the identical refusal back. That
45
+ * is the shape this repo treats as a defect in its own right: a cure that does not resolve the state it
46
+ * is prescribed for. Reported from the field, where a generated `design.html` sitting untracked made the
47
+ * command permanently unusable in that clone.
48
+ *
49
+ * Now the refusal only ever names tracked changes, so `git stash` — the cure it prints — always resolves
50
+ * it, by construction.
51
+ *
52
+ * ─── WHY `--untracked-files=no` AND `ls-files -z` RATHER THAN PARSING `??` ─────────────────────────────
53
+ * The decision half asks git the tracked-only question directly, so no line of porcelain output is ever
54
+ * parsed to make it. The reporting half asks `ls-files --others --exclude-standard -z`, which emits bare
55
+ * NUL-separated paths — no `?? ` prefix to strip and, crucially, no quoting: porcelain v1 wraps paths
56
+ * containing special characters in quotes with C-style escapes, so a hand-parser would hand back a
57
+ * mangled name for exactly the paths a human most needs to read correctly.
58
+ */
59
+ let WorkingTreeGate = class WorkingTreeGate {
60
+ /**
61
+ * Throw when tracked files are modified or staged; otherwise hand back the untracked files that were
62
+ * deliberately allowed through, for the caller to mention.
63
+ */
64
+ assertNoTrackedChanges(repoRoot) {
65
+ const tracked = this.git(repoRoot, ['status', '--porcelain', '--untracked-files=no']);
66
+ if (tracked !== null && tracked.trim() !== '') {
67
+ throw new rules_config_1.CliExitError(1, `${SEP}❌ Uncommitted changes to tracked files\n${SEP}\n`
68
+ + 'Going to main would carry them along. Commit them on this branch, or `git stash` them\n'
69
+ + '(everything listed below is tracked, which is exactly what a plain stash takes), then\n'
70
+ + 're-run. The webpieces tooling never commits your work for you.\n\n'
71
+ + `${tracked}`);
72
+ }
73
+ return this.untrackedFiles(repoRoot);
74
+ }
75
+ untrackedFiles(repoRoot) {
76
+ const output = this.git(repoRoot, ['ls-files', '--others', '--exclude-standard', '-z']);
77
+ if (output === null)
78
+ return new UntrackedFiles([]);
79
+ return new UntrackedFiles(output.split('\0').filter((path) => path !== ''));
80
+ }
81
+ /** Captured git output, or null when git could not be run or exited non-zero. */
82
+ git(repoRoot, args) {
83
+ const result = (0, child_process_1.spawnSync)('git', ['-C', repoRoot, ...args], {
84
+ encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
85
+ });
86
+ if (result.error !== undefined || result.status !== 0)
87
+ return null;
88
+ return result.stdout;
89
+ }
90
+ };
91
+ exports.WorkingTreeGate = WorkingTreeGate;
92
+ exports.WorkingTreeGate = WorkingTreeGate = tslib_1.__decorate([
93
+ (0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton)
94
+ ], WorkingTreeGate);
95
+ //# sourceMappingURL=working-tree-gate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"working-tree-gate.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/working-tree-gate.ts"],"names":[],"mappings":";;;;AAAA,iDAA0C;AAC1C,yCAA2D;AAC3D,0DAAuD;AAEvD,MAAM,GAAG,GAAG,0DAA0D,CAAC;AAEvE;;;GAGG;AACH,MAAa,cAAc;IACF;IAArB,YAAqB,KAAwB;QAAxB,UAAK,GAAL,KAAK,CAAmB;IAAG,CAAC;IAEjD,OAAO;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC;IACnC,CAAC;IAED,kEAAkE;IAClE,MAAM;QACF,IAAI,IAAI,CAAC,OAAO,EAAE;YAAE,OAAO,EAAE,CAAC;QAC9B,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAY,EAAU,EAAE,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAC9E,OAAO,0FAA0F;cAC3F,0CAA0C;cAC1C,IAAI,CAAC;IACf,CAAC;CACJ;AAfD,wCAeC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEI,IAAM,eAAe,GAArB,MAAM,eAAe;IACxB;;;OAGG;IACH,sBAAsB,CAAC,QAAgB;QACnC,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE,aAAa,EAAE,sBAAsB,CAAC,CAAC,CAAC;QACtF,IAAI,OAAO,KAAK,IAAI,IAAI,OAAO,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;YAC5C,MAAM,IAAI,2BAAY,CAAC,CAAC,EACpB,GAAG,GAAG,2CAA2C,GAAG,IAAI;kBACtD,yFAAyF;kBACzF,yFAAyF;kBACzF,oEAAoE;kBACpE,GAAG,OAAO,EAAE,CAAC,CAAC;QACxB,CAAC;QACD,OAAO,IAAI,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC;IACzC,CAAC;IAEO,cAAc,CAAC,QAAgB;QACnC,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC,UAAU,EAAE,UAAU,EAAE,oBAAoB,EAAE,IAAI,CAAC,CAAC,CAAC;QACxF,IAAI,MAAM,KAAK,IAAI;YAAE,OAAO,IAAI,cAAc,CAAC,EAAE,CAAC,CAAC;QACnD,OAAO,IAAI,cAAc,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,IAAY,EAAW,EAAE,CAAC,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC;IACjG,CAAC;IAED,iFAAiF;IACzE,GAAG,CAAC,QAAgB,EAAE,IAAc;QACxC,MAAM,MAAM,GAAG,IAAA,yBAAS,EAAC,KAAK,EAAE,CAAC,IAAI,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAC,EAAE;YACvD,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC;SACxD,CAAC,CAAC;QACH,IAAI,MAAM,CAAC,KAAK,KAAK,SAAS,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;QACnE,OAAO,MAAM,CAAC,MAAM,CAAC;IACzB,CAAC;CACJ,CAAA;AAhCY,0CAAe;0BAAf,eAAe;IAD3B,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;GAC5B,eAAe,CAgC3B","sourcesContent":["import { spawnSync } from 'child_process';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { CliExitError } from '@webpieces/rules-config';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * The untracked files a branch switch is about to leave exactly where they are — carried out of the gate\n * so the caller can SAY so, rather than surprise anyone with files that appear to have followed them.\n */\nexport class UntrackedFiles {\n constructor(readonly paths: readonly string[]) {}\n\n isEmpty(): boolean {\n return this.paths.length === 0;\n }\n\n /** The human-facing block, or '' when there is nothing to say. */\n render(): string {\n if (this.isEmpty()) return '';\n const list = this.paths.map((path: string): string => ` ${path}\\n`).join('');\n return 'These untracked files are not on any branch, so the checkout leaves them exactly where\\n'\n + 'they are — nothing about them changed:\\n'\n + list;\n }\n}\n\n/**\n * The clean-tree gate for `wp-checkout-clean-main`: refuse when TRACKED work would ride onto main, and\n * only then.\n *\n * ─── WHY UNTRACKED FILES ARE NOT A REASON TO REFUSE ───────────────────────────────────────────────────\n * The hazard the gate exists for is real but narrow: `git checkout` carries uncommitted changes to\n * TRACKED files across the switch, and main is the one place nobody wants to discover their work. An\n * untracked file has none of that. It is not on any branch, the switch does not move it, and no branch\n * can end up holding it — so refusing on one blocks a routine, safe operation.\n *\n * Worse, it was a refusal the user could not clear without deleting their file. The message prescribed\n * \"commit them on this branch, or stash them\", and a plain `git stash` does NOT take untracked files\n * (that needs `-u`) — so a user who did exactly what the error said got the identical refusal back. That\n * is the shape this repo treats as a defect in its own right: a cure that does not resolve the state it\n * is prescribed for. Reported from the field, where a generated `design.html` sitting untracked made the\n * command permanently unusable in that clone.\n *\n * Now the refusal only ever names tracked changes, so `git stash` — the cure it prints — always resolves\n * it, by construction.\n *\n * ─── WHY `--untracked-files=no` AND `ls-files -z` RATHER THAN PARSING `??` ─────────────────────────────\n * The decision half asks git the tracked-only question directly, so no line of porcelain output is ever\n * parsed to make it. The reporting half asks `ls-files --others --exclude-standard -z`, which emits bare\n * NUL-separated paths — no `?? ` prefix to strip and, crucially, no quoting: porcelain v1 wraps paths\n * containing special characters in quotes with C-style escapes, so a hand-parser would hand back a\n * mangled name for exactly the paths a human most needs to read correctly.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class WorkingTreeGate {\n /**\n * Throw when tracked files are modified or staged; otherwise hand back the untracked files that were\n * deliberately allowed through, for the caller to mention.\n */\n assertNoTrackedChanges(repoRoot: string): UntrackedFiles {\n const tracked = this.git(repoRoot, ['status', '--porcelain', '--untracked-files=no']);\n if (tracked !== null && tracked.trim() !== '') {\n throw new CliExitError(1,\n `${SEP}❌ Uncommitted changes to tracked files\\n${SEP}\\n`\n + 'Going to main would carry them along. Commit them on this branch, or `git stash` them\\n'\n + '(everything listed below is tracked, which is exactly what a plain stash takes), then\\n'\n + 're-run. The webpieces tooling never commits your work for you.\\n\\n'\n + `${tracked}`);\n }\n return this.untrackedFiles(repoRoot);\n }\n\n private untrackedFiles(repoRoot: string): UntrackedFiles {\n const output = this.git(repoRoot, ['ls-files', '--others', '--exclude-standard', '-z']);\n if (output === null) return new UntrackedFiles([]);\n return new UntrackedFiles(output.split('\\0').filter((path: string): boolean => path !== ''));\n }\n\n /** Captured git output, or null when git could not be run or exited non-zero. */\n private git(repoRoot: string, args: string[]): string | null {\n const result = spawnSync('git', ['-C', repoRoot, ...args], {\n encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],\n });\n if (result.error !== undefined || result.status !== 0) return null;\n return result.stdout;\n }\n}\n"]}
@@ -51,104 +51,6 @@ class SyncSlot {
51
51
  this.n = n;
52
52
  }
53
53
  }
54
- // Single source of truth for the merge process (written at conflict time, parameterized with the live
55
- // MERGE_DIR + conflicted-file list so it can never drift from the actual layout).
56
- const MERGE_PROCESS_TEMPLATE = `# AI-Assisted Squash-Merge Conflict Resolution (generated)
57
-
58
- This file was generated by \`pnpm {{START_COMMAND}}\` when the 3-point squash-merge hit conflicts.
59
- It is the single source of truth for the merge process — follow it exactly.
60
-
61
- You are on branch \`{{SQUASH_BRANCH}}\` with conflict markers in the working tree.
62
- \`MERGE_DIR = {{MERGE_DIR}}\`
63
-
64
- ## How the gate works
65
-
66
- - Resolve every conflicted file in the working tree.
67
- - Run **\`pnpm {{FINISH_COMMAND}}\`** — the validation + finish gate. It scans for leftover conflict
68
- markers, checks each conflicted file has a written merge explanation, validates, and commits the
69
- merge. It is the paired half of \`{{START_COMMAND}}\` — never finish with the other flow's command
70
- (\`wp-start-update\` pairs with \`wp-finish-update\`; \`wp-start-upsert-pr\` pairs with
71
- \`wp-finish-upsert-pr\`, which additionally runs the \`nx affected\` build, renders the dashboard,
72
- and creates/updates the PR).
73
- - **DO \`git add\` each file you resolve.** Staging your resolutions is your job, not the gate's, and
74
- \`git add\` is **not** blocked by any guard. The gate stages with \`git add -u\`, which only re-stages
75
- already-tracked paths — leave a resolution unstaged and the gate stops with "Git still reports
76
- unmerged files" and sends you back to \`git add\` it.
77
- - **Do NOT run \`git commit\` / \`git push\` / \`gh pr create|edit|merge\` yourself** (nor \`git merge\` /
78
- \`git rebase\`). The \`merge-in-progress-guard\` hook blocks exactly those until the gate validates.
79
- The gate does the commit. See \`webpieces.git-workflow.md\` → "Outcome B — CONFLICT merge" for the
80
- full who-does-what; this file does not restate it.
81
-
82
- ## STEP 1 — Load the merge context
83
-
84
- Per conflicted file, \`MERGE_DIR/updatemain-<safe_path>/\` holds (\`<safe_path>\` = path with \`/\`→\`__\`):
85
-
86
- \`\`\`
87
- A-forkpoint.txt # file at fork point (base)
88
- B-feature.txt # file on your feature branch
89
- C-main.txt # file on main
90
- B-A.diff # what your feature changed (B−A)
91
- C-A.diff # what main changed (C−A)
92
- \`\`\`
93
-
94
- \`updatemain-hashes.json\` holds A/B/C commit hashes. To see why main changed:
95
- \`git log <A>..<C> --oneline\`.
96
-
97
- Why A/B/C are trustworthy here — and why you must never \`git merge\`/\`git rebase\` main in yourself —
98
- is the fork-point invariant: see \`webpieces.git-workflow.md\` → "THE FORK POINT INVARIANT". Read it
99
- once if \`B-A.diff\` or \`C-A.diff\` ever shows you changes you do not recognize; that is the symptom
100
- of a polluted fork point, not of a bad diff.
101
-
102
- ## STEP 2 — Resolve each conflicted file
103
-
104
- For each file: read the working-tree file (the markers) and its \`B-A.diff\` / \`C-A.diff\` (intent),
105
- then Edit to the resolved version, removing ALL conflict markers.
106
-
107
- Strategies: goals align & non-overlapping → merge both · one side removes what the other modifies
108
- → prefer the removal · same lines, simple (imports/format) → merge both · same lines, complex or
109
- conflicting goals → ask the user · feature re-implements what main already squashed → prefer
110
- main's, then re-apply only the genuinely new feature work.
111
-
112
- **Then write a merge explanation** for each conflicted file — NOT a comment in the source (that
113
- breaks for JSON and deleted files). Write it next to that file's diffs, at:
114
-
115
- \`\`\`
116
- MERGE_DIR/updatemain-<safe_path>/{{EXPLANATION_FILE}}
117
- \`\`\`
118
-
119
- (\`<safe_path>\` = the conflict file path with \`/\` → \`__\`, the same dir that holds its
120
- \`A-forkpoint.txt\` / \`B-A.diff\` / \`C-A.diff\`.) In it, explain in a few sentences how you resolved
121
- this file: which side you took where, what you combined from B-A.diff vs C-A.diff, and why. The
122
- gate fails if any conflicted file's explanation is missing or empty. Do not paste A/B/C context
123
- blocks into the source code.
124
-
125
- ## STEP 3 — Run the gate (validates the merge AND finalizes it)
126
-
127
- \`\`\`
128
- pnpm {{FINISH_COMMAND}}
129
- \`\`\`
130
-
131
- - Leftover conflict markers → fix those files and re-run.
132
- - Missing merge explanation → write it (see STEP 2) and re-run.
133
- - Build failure → fix the TypeScript/lint errors and re-run (the gate re-stages for you).
134
- - Missing review.json (PR flow only) → write it in the printed format (your PR review), then re-run.
135
- - On success it commits and finalizes the merge (in the PR flow it also renders the dashboard and
136
- creates/updates the PR).
137
-
138
- ## Conflicted files
139
-
140
- {{FILE_LIST}}
141
-
142
- ## If you need to bail out
143
-
144
- A numbered backup branch was created (e.g. \`<feature>PreMerge1\`). To abandon:
145
-
146
- \`\`\`
147
- git merge --abort 2>/dev/null; git checkout <feature> ; git branch -D {{SQUASH_BRANCH}}
148
- \`\`\`
149
-
150
- Then delete \`{{MERGE_DIR}}/\` for a clean slate.
151
- `;
152
54
  // merge-START: the first half of the 3-point squash-merge lifecycle. Brings origin/main into a fresh
153
55
  // `<branch>Squash`, and on conflict writes the 3-point context + unvalidated marker + process doc,
154
56
  // then hands control back to the AI. On a clean merge it commits the squash and returns the branch
@@ -263,21 +165,19 @@ let MergeStart = class MergeStart {
263
165
  fs.writeFileSync(path.join(fileDir, 'C-A.diff'), ca.stdout ?? '');
264
166
  }
265
167
  }
168
+ // The LIVE rendering of the same template `.webpieces/instruct-ai/webpieces.mergeprocess.md` is
169
+ // delivered from — see rules-config/src/merge-process-doc.ts. The text lives there, not here, so
170
+ // the copy every repo receives and the copy a conflicted merge stamps are the same document.
266
171
  mergeProcessDoc(mergeDir, squashBranch, conflictedFiles, finishCommand) {
267
172
  const fileList = conflictedFiles.map((f) => `- \`${f}\``).join('\n');
268
- return MERGE_PROCESS_TEMPLATE
269
- .replace(/\{\{SQUASH_BRANCH\}\}/g, squashBranch)
270
- .replace(/\{\{MERGE_DIR\}\}/g, mergeDir)
271
- .replace(/\{\{EXPLANATION_FILE\}\}/g, rules_config_1.MERGE_EXPLANATION_FILE)
272
- .replace(/\{\{FINISH_COMMAND\}\}/g, finishCommand)
273
- .replace(/\{\{START_COMMAND\}\}/g, new rules_config_1.SyncFlowGuidance().pairedStart(finishCommand))
274
- .replace(/\{\{FILE_LIST\}\}/g, fileList);
173
+ const run = new rules_config_1.MergeRun(new rules_config_1.SyncFlowGuidance().pairedStart(finishCommand), finishCommand, squashBranch, mergeDir, fileList);
174
+ return new rules_config_1.MergeProcessText((0, rules_config_1.loadTemplate)(rules_config_1.MERGE_PROCESS_DOC)).render(run);
275
175
  }
276
176
  // Returns the absolute path of the written doc.
277
177
  writeMergeProcessDoc(repoRoot, mergeDir, squashBranch, conflictedFiles, finishCommand) {
278
178
  const docDir = rules_config_1.dotWebpieces.localFile(repoRoot, 'instruct-ai');
279
179
  fs.mkdirSync(docDir, { recursive: true });
280
- const docPath = path.join(docDir, 'webpieces.mergeprocess.md');
180
+ const docPath = path.join(docDir, rules_config_1.MERGE_PROCESS_DOC);
281
181
  fs.writeFileSync(docPath, this.mergeProcessDoc(mergeDir, squashBranch, conflictedFiles, finishCommand));
282
182
  return docPath;
283
183
  }
@@ -336,7 +236,7 @@ let MergeStart = class MergeStart {
336
236
  event.conflictFiles = files;
337
237
  event.artifacts = [
338
238
  path.join(mergeDir, 'updatemain-<file>'),
339
- rules_config_1.dotWebpieces.localFile(repoRoot, 'instruct-ai', 'webpieces.mergeprocess.md'),
239
+ rules_config_1.dotWebpieces.localFile(repoRoot, 'instruct-ai', rules_config_1.MERGE_PROCESS_DOC),
340
240
  ];
341
241
  (0, rules_config_1.logBranchMutation)(repoRoot, event);
342
242
  }
@@ -1 +1 @@
1
- {"version":3,"file":"merge-start.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/merge-start.ts"],"names":[],"mappings":"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","sourcesContent":["import { execSync, spawnSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport {\n dotWebpieces, MERGE_EXPLANATION_FILE, CliExitError,\n MutationVerb, BranchMutationEvent, logBranchMutation, SyncFlowGuidance,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { GatherInfo } from '../git-gatherInfo';\nimport { BranchNaming } from './branch-naming';\nimport { GitExec } from './git-exec';\nimport { MergeState, MergeMarker } from './merge-state';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\ninterface HashPoints {\n hashForkPoint: string;\n hashFeatureHead: string;\n hashMainHead: string;\n}\n\n// The four branch names merge-END needs to finalize a merge (swap squash→feature, push, clean up).\nexport class MergeContext {\n currentBranch: string;\n squashBranch: string;\n backupBranch: string;\n prNumber: string;\n\n constructor(currentBranch: string, squashBranch: string, backupBranch: string, prNumber: string) {\n this.currentBranch = currentBranch;\n this.squashBranch = squashBranch;\n this.backupBranch = backupBranch;\n this.prNumber = prNumber;\n }\n}\n\n// Outcome of merge-start: 'clean' carries the context for merge-END to finalize; 'conflict' means the\n// marker + context files were written and the caller should hand back to the AI (exit 2).\nexport class MergeStartResult {\n status: 'clean' | 'conflict';\n context: MergeContext | null;\n runDir: string; // this sync's numbered `merge-<n>/` dir — passed to merge-END so it reads THIS marker\n\n constructor(status: 'clean' | 'conflict', context: MergeContext | null, runDir: string) {\n this.status = status;\n this.context = context;\n this.runDir = runDir;\n }\n}\n\n// The one number `n` for a sync and the things it names: the pre-merge backup branch, its paired\n// conflict-context run dir, and (via `n`) the `preMerge<n>.hash` record of the tip it snapshotted.\nclass SyncSlot {\n backupBranch: string;\n runDir: string;\n n: number;\n\n constructor(backupBranch: string, runDir: string, n: number) {\n this.backupBranch = backupBranch;\n this.runDir = runDir;\n this.n = n;\n }\n}\n\n// Single source of truth for the merge process (written at conflict time, parameterized with the live\n// MERGE_DIR + conflicted-file list so it can never drift from the actual layout).\nconst MERGE_PROCESS_TEMPLATE = `# AI-Assisted Squash-Merge Conflict Resolution (generated)\n\nThis file was generated by \\`pnpm {{START_COMMAND}}\\` when the 3-point squash-merge hit conflicts.\nIt is the single source of truth for the merge process — follow it exactly.\n\nYou are on branch \\`{{SQUASH_BRANCH}}\\` with conflict markers in the working tree.\n\\`MERGE_DIR = {{MERGE_DIR}}\\`\n\n## How the gate works\n\n- Resolve every conflicted file in the working tree.\n- Run **\\`pnpm {{FINISH_COMMAND}}\\`** — the validation + finish gate. It scans for leftover conflict\n markers, checks each conflicted file has a written merge explanation, validates, and commits the\n merge. It is the paired half of \\`{{START_COMMAND}}\\` — never finish with the other flow's command\n (\\`wp-start-update\\` pairs with \\`wp-finish-update\\`; \\`wp-start-upsert-pr\\` pairs with\n \\`wp-finish-upsert-pr\\`, which additionally runs the \\`nx affected\\` build, renders the dashboard,\n and creates/updates the PR).\n- **DO \\`git add\\` each file you resolve.** Staging your resolutions is your job, not the gate's, and\n \\`git add\\` is **not** blocked by any guard. The gate stages with \\`git add -u\\`, which only re-stages\n already-tracked paths — leave a resolution unstaged and the gate stops with \"Git still reports\n unmerged files\" and sends you back to \\`git add\\` it.\n- **Do NOT run \\`git commit\\` / \\`git push\\` / \\`gh pr create|edit|merge\\` yourself** (nor \\`git merge\\` /\n \\`git rebase\\`). The \\`merge-in-progress-guard\\` hook blocks exactly those until the gate validates.\n The gate does the commit. See \\`webpieces.git-workflow.md\\` → \"Outcome B — CONFLICT merge\" for the\n full who-does-what; this file does not restate it.\n\n## STEP 1 — Load the merge context\n\nPer conflicted file, \\`MERGE_DIR/updatemain-<safe_path>/\\` holds (\\`<safe_path>\\` = path with \\`/\\`→\\`__\\`):\n\n\\`\\`\\`\nA-forkpoint.txt # file at fork point (base)\nB-feature.txt # file on your feature branch\nC-main.txt # file on main\nB-A.diff # what your feature changed (B−A)\nC-A.diff # what main changed (C−A)\n\\`\\`\\`\n\n\\`updatemain-hashes.json\\` holds A/B/C commit hashes. To see why main changed:\n\\`git log <A>..<C> --oneline\\`.\n\nWhy A/B/C are trustworthy here — and why you must never \\`git merge\\`/\\`git rebase\\` main in yourself —\nis the fork-point invariant: see \\`webpieces.git-workflow.md\\` → \"THE FORK POINT INVARIANT\". Read it\nonce if \\`B-A.diff\\` or \\`C-A.diff\\` ever shows you changes you do not recognize; that is the symptom\nof a polluted fork point, not of a bad diff.\n\n## STEP 2 — Resolve each conflicted file\n\nFor each file: read the working-tree file (the markers) and its \\`B-A.diff\\` / \\`C-A.diff\\` (intent),\nthen Edit to the resolved version, removing ALL conflict markers.\n\nStrategies: goals align & non-overlapping → merge both · one side removes what the other modifies\n→ prefer the removal · same lines, simple (imports/format) → merge both · same lines, complex or\nconflicting goals → ask the user · feature re-implements what main already squashed → prefer\nmain's, then re-apply only the genuinely new feature work.\n\n**Then write a merge explanation** for each conflicted file — NOT a comment in the source (that\nbreaks for JSON and deleted files). Write it next to that file's diffs, at:\n\n\\`\\`\\`\nMERGE_DIR/updatemain-<safe_path>/{{EXPLANATION_FILE}}\n\\`\\`\\`\n\n(\\`<safe_path>\\` = the conflict file path with \\`/\\` → \\`__\\`, the same dir that holds its\n\\`A-forkpoint.txt\\` / \\`B-A.diff\\` / \\`C-A.diff\\`.) In it, explain in a few sentences how you resolved\nthis file: which side you took where, what you combined from B-A.diff vs C-A.diff, and why. The\ngate fails if any conflicted file's explanation is missing or empty. Do not paste A/B/C context\nblocks into the source code.\n\n## STEP 3 — Run the gate (validates the merge AND finalizes it)\n\n\\`\\`\\`\npnpm {{FINISH_COMMAND}}\n\\`\\`\\`\n\n- Leftover conflict markers → fix those files and re-run.\n- Missing merge explanation → write it (see STEP 2) and re-run.\n- Build failure → fix the TypeScript/lint errors and re-run (the gate re-stages for you).\n- Missing review.json (PR flow only) → write it in the printed format (your PR review), then re-run.\n- On success it commits and finalizes the merge (in the PR flow it also renders the dashboard and\n creates/updates the PR).\n\n## Conflicted files\n\n{{FILE_LIST}}\n\n## If you need to bail out\n\nA numbered backup branch was created (e.g. \\`<feature>PreMerge1\\`). To abandon:\n\n\\`\\`\\`\ngit merge --abort 2>/dev/null; git checkout <feature> ; git branch -D {{SQUASH_BRANCH}}\n\\`\\`\\`\n\nThen delete \\`{{MERGE_DIR}}/\\` for a clean slate.\n`;\n\n// merge-START: the first half of the 3-point squash-merge lifecycle. Brings origin/main into a fresh\n// `<branch>Squash`, and on conflict writes the 3-point context + unvalidated marker + process doc,\n// then hands control back to the AI. On a clean merge it commits the squash and returns the branch\n// context so the caller (RunUpdate) can run merge-END to finalize. NEVER finalizes or posts a PR.\n@injectable(bindingScopeValues.Singleton)\nexport class MergeStart {\n constructor(\n private readonly gatherInfo: GatherInfo,\n private readonly branchNaming: BranchNaming,\n private readonly gitExec: GitExec,\n private readonly mergeState: MergeState,\n ) {}\n\n async mergeStart(repoRoot: string, verb: MutationVerb, home: string, finishCommand: string): Promise<MergeStartResult> {\n const currentBranch = execSync('git branch --show-current', { encoding: 'utf8' }).trim();\n if (currentBranch.endsWith('Squash')) {\n throw new CliExitError(1, `❌ On a leftover ${currentBranch} branch with no merge marker. Clean up: git branch -D ${currentBranch}`);\n }\n\n // One number `n` for this sync drives BOTH the backup branch and its `merge-<n>/` context dir.\n const slot = this.chooseSyncSlot(home, currentBranch);\n const backupBranch = slot.backupBranch;\n const mergeDir = slot.runDir;\n\n process.stdout.write('\\n' + SEP + '🔄 Squash-Merge Update from Main\\n' + SEP + '\\n');\n const info = await this.gatherInfo.gatherInfo();\n const hashes = info.hashes;\n if (info.alreadyUpToDate) {\n process.stdout.write('ℹ️ Branch already even with main; nothing to merge, continuing to push/build.\\n');\n }\n\n const prNumber = this.detectPr(this.branchNaming.baseBranchName(currentBranch));\n process.stdout.write(prNumber ? `Existing PR #${prNumber} will be updated.\\n` : 'No existing PR (one can be created later).\\n');\n\n this.createBackup(currentBranch, backupBranch);\n // Record THIS sync's pre-merge tip as `staged/<feature>/preMerge<n>.hash` — every intermediate\n // state, not just the last one. It is what lets the `<feature>PreMerge<n>` snapshot BRANCH be\n // disposable (branches count toward the branch cap; a written-down hash does not), and it is the\n // ref the archive tag is cut from when the PR lands.\n this.mergeState.writePreMergeHash(home, slot.n, this.fullSha(currentBranch));\n const backupEvent = new BranchMutationEvent(verb, 'BACKUP');\n backupEvent.fromBranch = currentBranch;\n backupEvent.toBranch = backupBranch;\n logBranchMutation(repoRoot, backupEvent);\n\n const squashBranch = `${currentBranch}Squash`;\n if (spawnSync('git', ['show-ref', '--verify', '--quiet', `refs/heads/${squashBranch}`]).status === 0) {\n throw new CliExitError(1, `❌ Stale ${squashBranch} from a previous run. Delete it: git branch -D ${squashBranch}`);\n }\n\n // Branch the squash off origin/main directly — worktree-native. origin/main was already fetched\n // in gatherInfo(), and A/B/C are computed purely from origin/main, so the merge base is identical\n // to the old checkout-main + pull path.\n const originMainSha = this.shortSha('origin/main');\n this.gitExec.runGitChecked(['checkout', '-b', squashBranch, 'origin/main'], 'Failed to create squash branch off origin/main');\n const baseEvent = new BranchMutationEvent(verb, 'PULL');\n baseEvent.newMain = originMainSha;\n logBranchMutation(repoRoot, baseEvent);\n\n process.stdout.write('\\n' + SEP + `🔀 Squash merging ${currentBranch}\\n` + SEP + '\\n');\n const merge = spawnSync('git', ['merge', '--squash', currentBranch], { stdio: 'inherit' });\n if (merge.status !== 0) {\n this.handleConflictsHandback(repoRoot, mergeDir, currentBranch, squashBranch, backupBranch, prNumber, hashes, finishCommand);\n this.logConflict(repoRoot, verb, mergeDir);\n return new MergeStartResult('conflict', null, mergeDir);\n }\n logBranchMutation(repoRoot, new BranchMutationEvent(verb, 'SQUASH'));\n\n const nothingStaged = spawnSync('git', ['diff-index', '--quiet', '--cached', 'HEAD', '--']).status === 0;\n if (nothingStaged) {\n process.stdout.write('ℹ️ Already up-to-date with main (nothing to merge).\\n');\n } else {\n // Internal, transient subject for the single squash commit on the feature branch. It NO LONGER\n // reaches main's history: finish-upsert-pr squash-merges the PR with an explicit\n // `gh pr merge --subject <PR title> --body-file <commit summary>`, so main carries the PR title.\n this.gitExec.runGitChecked(['commit', '-m', `Squash merge of ${currentBranch}`], 'Failed to commit squash merge');\n // Clean merge ⇒ hashes only, no conflicts.md. Its ABSENCE is what marks this merge clean.\n this.mergeState.recordCleanMerge(mergeDir, hashes.hashForkPoint, hashes.hashFeatureHead, hashes.hashMainHead);\n }\n return new MergeStartResult('clean', new MergeContext(currentBranch, squashBranch, backupBranch, prNumber), mergeDir);\n }\n\n // Detect the PR by its STABLE feature branch (a leftover `…wpN` still resolves to the one name the\n // PR lives on).\n private detectPr(baseBranch: string): string {\n const result = spawnSync(\n 'gh', ['pr', 'list', '--head', baseBranch, '--json', 'number', '--jq', '.[0].number'],\n { encoding: 'utf8' },\n );\n return result.status === 0 ? (result.stdout ?? '').trim() : '';\n }\n\n // Pick this sync's slot number MONOTONICALLY from the durable audit dirs, then step past any\n // existing `<branch>PreMerge<n>` branch so the same `n` is free for the paired backup branch.\n private chooseSyncSlot(home: string, currentBranch: string): SyncSlot {\n const branchExists = (name: string): boolean =>\n spawnSync('git', ['show-ref', '--verify', '--quiet', `refs/heads/${name}`]).status === 0;\n let n = this.mergeState.nextMergeSlotNumber(home);\n while (branchExists(this.branchNaming.preMergeBackupName(currentBranch, n))) n += 1;\n return new SyncSlot(this.branchNaming.preMergeBackupName(currentBranch, n), this.mergeState.mergeRunDirFor(home, n), n);\n }\n\n // Snapshot the pre-merge state onto the caller-chosen `backupBranch`, never overwriting.\n private createBackup(currentBranch: string, backupBranch: string): void {\n process.stdout.write('\\n' + SEP + '💾 Creating Pre-Merge Backup\\n' + SEP + '\\n');\n this.gitExec.runGitChecked(['checkout', '-b', backupBranch], 'Failed to create backup branch');\n this.gitExec.runGitChecked(['checkout', currentBranch], 'Failed to return to feature branch');\n process.stdout.write(`✅ Backup created: ${backupBranch}\\n\\n`);\n }\n\n private saveConflictContext(\n conflictedFiles: string[], mergeDir: string, forkPoint: string, featureHead: string, mainHead: string,\n ): void {\n for (const file of conflictedFiles) {\n const fileDir = this.mergeState.perFileContextDir(mergeDir, file);\n fs.mkdirSync(fileDir, { recursive: true });\n\n const fork = spawnSync('git', ['show', `${forkPoint}:${file}`], { encoding: 'utf8' });\n fs.writeFileSync(path.join(fileDir, 'A-forkpoint.txt'), fork.status === 0 ? (fork.stdout ?? '') : '(file did not exist)\\n');\n const feature = spawnSync('git', ['show', `${featureHead}:${file}`], { encoding: 'utf8' });\n fs.writeFileSync(path.join(fileDir, 'B-feature.txt'), feature.status === 0 ? (feature.stdout ?? '') : '(file did not exist)\\n');\n const main = spawnSync('git', ['show', `${mainHead}:${file}`], { encoding: 'utf8' });\n fs.writeFileSync(path.join(fileDir, 'C-main.txt'), main.status === 0 ? (main.stdout ?? '') : '(file did not exist)\\n');\n\n const ba = spawnSync('git', ['diff', forkPoint, featureHead, '--', file], { encoding: 'utf8' });\n fs.writeFileSync(path.join(fileDir, 'B-A.diff'), ba.stdout ?? '');\n const ca = spawnSync('git', ['diff', forkPoint, mainHead, '--', file], { encoding: 'utf8' });\n fs.writeFileSync(path.join(fileDir, 'C-A.diff'), ca.stdout ?? '');\n }\n }\n\n private mergeProcessDoc(mergeDir: string, squashBranch: string, conflictedFiles: string[], finishCommand: string): string {\n const fileList = conflictedFiles.map((f: string): string => `- \\`${f}\\``).join('\\n');\n return MERGE_PROCESS_TEMPLATE\n .replace(/\\{\\{SQUASH_BRANCH\\}\\}/g, squashBranch)\n .replace(/\\{\\{MERGE_DIR\\}\\}/g, mergeDir)\n .replace(/\\{\\{EXPLANATION_FILE\\}\\}/g, MERGE_EXPLANATION_FILE)\n .replace(/\\{\\{FINISH_COMMAND\\}\\}/g, finishCommand)\n .replace(/\\{\\{START_COMMAND\\}\\}/g, new SyncFlowGuidance().pairedStart(finishCommand))\n .replace(/\\{\\{FILE_LIST\\}\\}/g, fileList);\n }\n\n // Returns the absolute path of the written doc.\n private writeMergeProcessDoc(repoRoot: string, mergeDir: string, squashBranch: string, conflictedFiles: string[], finishCommand: string): string {\n const docDir = dotWebpieces.localFile(repoRoot, 'instruct-ai');\n fs.mkdirSync(docDir, { recursive: true });\n const docPath = path.join(docDir, 'webpieces.mergeprocess.md');\n fs.writeFileSync(docPath, this.mergeProcessDoc(mergeDir, squashBranch, conflictedFiles, finishCommand));\n return docPath;\n }\n\n // The AI-facing \"what just happened / what to do next\" recap on the conflict path.\n private printConflictHandback(\n docPath: string, mergeDir: string, squashBranch: string, conflictedFiles: string[], finishCommand: string,\n ): void {\n process.stdout.write('\\n' + SEP + `⚠️ Conflicts in ${conflictedFiles.length} file(s) — handing control back to you\\n` + SEP + '\\n');\n process.stdout.write('Here is exactly what I did and what you need to do:\\n\\n');\n process.stdout.write('What I did:\\n');\n process.stdout.write(' 1. snapshotted your pre-merge state to a PreMerge branch\\n');\n process.stdout.write(' 2. pulled origin/main and squash-merged your work onto it\\n');\n process.stdout.write(` 3. hit conflicts — you are now on the transient branch ${squashBranch}\\n\\n`);\n process.stdout.write('What you need to do:\\n');\n process.stdout.write(` 1. read the merge process doc: ${docPath}\\n`);\n process.stdout.write(` 2. resolve each conflicted file below (its 3-point A/B/C context + diffs are in\\n`);\n process.stdout.write(` ${mergeDir}/updatemain-<file>/), \\`git add\\` it, and write that file's merge-explanation.md\\n`);\n process.stdout.write(` 3. run pnpm ${finishCommand} — it validates, commits, and finalizes (do NOT git commit/push yourself)\\n\\n`);\n process.stdout.write('Conflicted files:\\n');\n for (const file of conflictedFiles) process.stdout.write(` - ${file}\\n`);\n process.stdout.write('\\n' + SEP);\n }\n\n // Write the conflict context files + the unvalidated marker + the process doc. Does NOT exit.\n private handleConflictsHandback(\n repoRoot: string, mergeDir: string, currentBranch: string, squashBranch: string,\n backupBranch: string, prNumber: string, hashes: HashPoints, finishCommand: string,\n ): void {\n const raw = execSync('git diff --name-only --diff-filter=U', { encoding: 'utf8' }).trim();\n const conflictedFiles = raw.split('\\n').filter((f: string): boolean => f.trim() !== '');\n fs.mkdirSync(mergeDir, { recursive: true });\n // `conflicts.md` replaces the old `updatemain-conflicted-files.txt` (which nothing read) AND is\n // the 3-point signal itself: this file exists in a run dir if and only if that merge conflicted,\n // which is what lets the index classify each merge without a per-branch directory-name scheme.\n this.mergeState.writeConflicts(mergeDir, conflictedFiles);\n fs.writeFileSync(path.join(mergeDir, 'updatemain-hashes.json'), JSON.stringify(hashes, null, 2) + '\\n');\n this.saveConflictContext(conflictedFiles, mergeDir, hashes.hashForkPoint, hashes.hashFeatureHead, hashes.hashMainHead);\n\n const marker = new MergeMarker(\n currentBranch, squashBranch, backupBranch, prNumber, conflictedFiles,\n hashes.hashForkPoint, hashes.hashFeatureHead, hashes.hashMainHead, false,\n );\n this.mergeState.writeMergeMarker(mergeDir, marker);\n const docPath = this.writeMergeProcessDoc(repoRoot, mergeDir, squashBranch, conflictedFiles, finishCommand);\n this.printConflictHandback(docPath, mergeDir, squashBranch, conflictedFiles, finishCommand);\n }\n\n // Short sha of a ref (best-effort — '' if it can't resolve).\n private shortSha(ref: string): string {\n const result = spawnSync('git', ['rev-parse', '--short', ref], { encoding: 'utf8' });\n return result.status === 0 ? (result.stdout ?? '').trim() : '';\n }\n\n // FULL sha of a ref (best-effort — '' if it can't resolve). Recorded for the pre-merge tips because\n // those hashes are meant to be used later to restore state, and an abbreviated sha can go ambiguous\n // as the repo grows.\n private fullSha(ref: string): string {\n const result = spawnSync('git', ['rev-parse', ref], { encoding: 'utf8' });\n return result.status === 0 ? (result.stdout ?? '').trim() : '';\n }\n\n // Log the CONFLICT phase with the conflicted-file list + artifact paths a resolver needs.\n private logConflict(repoRoot: string, verb: MutationVerb, mergeDir: string): void {\n const raw = spawnSync('git', ['diff', '--name-only', '--diff-filter=U'], { cwd: repoRoot, encoding: 'utf8' });\n const files = (raw.status === 0 ? (raw.stdout ?? '') : '').split('\\n').map((f: string): string => f.trim()).filter((f: string): boolean => f !== '');\n const event = new BranchMutationEvent(verb, 'CONFLICT');\n event.conflict = true;\n event.conflictFiles = files;\n event.artifacts = [\n path.join(mergeDir, 'updatemain-<file>'),\n dotWebpieces.localFile(repoRoot, 'instruct-ai', 'webpieces.mergeprocess.md'),\n ];\n logBranchMutation(repoRoot, event);\n }\n}\n"]}
1
+ {"version":3,"file":"merge-start.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/merge-start.ts"],"names":[],"mappings":";;;;AAAA,iDAAoD;AACpD,+CAAyB;AACzB,mDAA6B;AAC7B,0DAIiC;AACjC,yCAA2D;AAC3D,sDAA+C;AAC/C,mDAA+C;AAC/C,yCAAqC;AACrC,+CAAwD;AAExD,MAAM,GAAG,GAAG,0DAA0D,CAAC;AAQvE,mGAAmG;AACnG,MAAa,YAAY;IACrB,aAAa,CAAS;IACtB,YAAY,CAAS;IACrB,YAAY,CAAS;IACrB,QAAQ,CAAS;IAEjB,YAAY,aAAqB,EAAE,YAAoB,EAAE,YAAoB,EAAE,QAAgB;QAC3F,IAAI,CAAC,aAAa,GAAG,aAAa,CAAC;QACnC,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;QACjC,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;QACjC,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC7B,CAAC;CACJ;AAZD,oCAYC;AAED,sGAAsG;AACtG,0FAA0F;AAC1F,MAAa,gBAAgB;IACzB,MAAM,CAAuB;IAC7B,OAAO,CAAsB;IAC7B,MAAM,CAAS,CAAC,sFAAsF;IAEtG,YAAY,MAA4B,EAAE,OAA4B,EAAE,MAAc;QAClF,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACzB,CAAC;CACJ;AAVD,4CAUC;AAED,iGAAiG;AACjG,mGAAmG;AACnG,MAAM,QAAQ;IACV,YAAY,CAAS;IACrB,MAAM,CAAS;IACf,CAAC,CAAS;IAEV,YAAY,YAAoB,EAAE,MAAc,EAAE,CAAS;QACvD,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;QACjC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;IACf,CAAC;CACJ;AAGD,qGAAqG;AACrG,mGAAmG;AACnG,mGAAmG;AACnG,kGAAkG;AAE3F,IAAM,UAAU,GAAhB,MAAM,UAAU;IAEE;IACA;IACA;IACA;IAJrB,YACqB,UAAsB,EACtB,YAA0B,EAC1B,OAAgB,EAChB,UAAsB;QAHtB,eAAU,GAAV,UAAU,CAAY;QACtB,iBAAY,GAAZ,YAAY,CAAc;QAC1B,YAAO,GAAP,OAAO,CAAS;QAChB,eAAU,GAAV,UAAU,CAAY;IACxC,CAAC;IAEJ,KAAK,CAAC,UAAU,CAAC,QAAgB,EAAE,IAAkB,EAAE,IAAY,EAAE,aAAqB;QACtF,MAAM,aAAa,GAAG,IAAA,wBAAQ,EAAC,2BAA2B,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QACzF,IAAI,aAAa,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;YACnC,MAAM,IAAI,2BAAY,CAAC,CAAC,EAAE,mBAAmB,aAAa,yDAAyD,aAAa,EAAE,CAAC,CAAC;QACxI,CAAC;QAED,+FAA+F;QAC/F,MAAM,IAAI,GAAG,IAAI,CAAC,cAAc,CAAC,IAAI,EAAE,aAAa,CAAC,CAAC;QACtD,MAAM,YAAY,GAAG,IAAI,CAAC,YAAY,CAAC;QACvC,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC;QAE7B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,GAAG,GAAG,GAAG,oCAAoC,GAAG,GAAG,GAAG,IAAI,CAAC,CAAC;QACrF,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,UAAU,EAAE,CAAC;QAChD,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;QAC3B,IAAI,IAAI,CAAC,eAAe,EAAE,CAAC;YACvB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,kFAAkF,CAAC,CAAC;QAC7G,CAAC;QAED,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,YAAY,CAAC,cAAc,CAAC,aAAa,CAAC,CAAC,CAAC;QAChF,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,gBAAgB,QAAQ,qBAAqB,CAAC,CAAC,CAAC,8CAA8C,CAAC,CAAC;QAEhI,IAAI,CAAC,YAAY,CAAC,aAAa,EAAE,YAAY,CAAC,CAAC;QAC/C,+FAA+F;QAC/F,8FAA8F;QAC9F,iGAAiG;QACjG,qDAAqD;QACrD,IAAI,CAAC,UAAU,CAAC,iBAAiB,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC;QAC7E,MAAM,WAAW,GAAG,IAAI,kCAAmB,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;QAC5D,WAAW,CAAC,UAAU,GAAG,aAAa,CAAC;QACvC,WAAW,CAAC,QAAQ,GAAG,YAAY,CAAC;QACpC,IAAA,gCAAiB,EAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;QAEzC,MAAM,YAAY,GAAG,GAAG,aAAa,QAAQ,CAAC;QAC9C,IAAI,IAAA,yBAAS,EAAC,KAAK,EAAE,CAAC,UAAU,EAAE,UAAU,EAAE,SAAS,EAAE,cAAc,YAAY,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACnG,MAAM,IAAI,2BAAY,CAAC,CAAC,EAAE,WAAW,YAAY,kDAAkD,YAAY,EAAE,CAAC,CAAC;QACvH,CAAC;QAED,gGAAgG;QAChG,kGAAkG;QAClG,wCAAwC;QACxC,MAAM,aAAa,GAAG,IAAI,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAC;QACnD,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC,UAAU,EAAE,IAAI,EAAE,YAAY,EAAE,aAAa,CAAC,EAAE,gDAAgD,CAAC,CAAC;QAC9H,MAAM,SAAS,GAAG,IAAI,kCAAmB,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QACxD,SAAS,CAAC,OAAO,GAAG,aAAa,CAAC;QAClC,IAAA,gCAAiB,EAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;QAEvC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,GAAG,GAAG,GAAG,qBAAqB,aAAa,IAAI,GAAG,GAAG,GAAG,IAAI,CAAC,CAAC;QACvF,MAAM,KAAK,GAAG,IAAA,yBAAS,EAAC,KAAK,EAAE,CAAC,OAAO,EAAE,UAAU,EAAE,aAAa,CAAC,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;QAC3F,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACrB,IAAI,CAAC,uBAAuB,CAAC,QAAQ,EAAE,QAAQ,EAAE,aAAa,EAAE,YAAY,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,EAAE,aAAa,CAAC,CAAC;YAC7H,IAAI,CAAC,WAAW,CAAC,QAAQ,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC;YAC3C,OAAO,IAAI,gBAAgB,CAAC,UAAU,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC;QAC5D,CAAC;QACD,IAAA,gCAAiB,EAAC,QAAQ,EAAE,IAAI,kCAAmB,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC;QAErE,MAAM,aAAa,GAAG,IAAA,yBAAS,EAAC,KAAK,EAAE,CAAC,YAAY,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC;QACzG,IAAI,aAAa,EAAE,CAAC;YAChB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,wDAAwD,CAAC,CAAC;QACnF,CAAC;aAAM,CAAC;YACJ,+FAA+F;YAC/F,iFAAiF;YACjF,iGAAiG;YACjG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC,QAAQ,EAAE,IAAI,EAAE,mBAAmB,aAAa,EAAE,CAAC,EAAE,+BAA+B,CAAC,CAAC;YAClH,0FAA0F;YAC1F,IAAI,CAAC,UAAU,CAAC,gBAAgB,CAAC,QAAQ,EAAE,MAAM,CAAC,aAAa,EAAE,MAAM,CAAC,eAAe,EAAE,MAAM,CAAC,YAAY,CAAC,CAAC;QAClH,CAAC;QACD,OAAO,IAAI,gBAAgB,CAAC,OAAO,EAAE,IAAI,YAAY,CAAC,aAAa,EAAE,YAAY,EAAE,YAAY,EAAE,QAAQ,CAAC,EAAE,QAAQ,CAAC,CAAC;IAC1H,CAAC;IAED,mGAAmG;IACnG,gBAAgB;IACR,QAAQ,CAAC,UAAkB;QAC/B,MAAM,MAAM,GAAG,IAAA,yBAAS,EACpB,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;QACF,OAAO,MAAM,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IACnE,CAAC;IAED,6FAA6F;IAC7F,8FAA8F;IACtF,cAAc,CAAC,IAAY,EAAE,aAAqB;QACtD,MAAM,YAAY,GAAG,CAAC,IAAY,EAAW,EAAE,CAC3C,IAAA,yBAAS,EAAC,KAAK,EAAE,CAAC,UAAU,EAAE,UAAU,EAAE,SAAS,EAAE,cAAc,IAAI,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC;QAC7F,IAAI,CAAC,GAAG,IAAI,CAAC,UAAU,CAAC,mBAAmB,CAAC,IAAI,CAAC,CAAC;QAClD,OAAO,YAAY,CAAC,IAAI,CAAC,YAAY,CAAC,kBAAkB,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC;YAAE,CAAC,IAAI,CAAC,CAAC;QACpF,OAAO,IAAI,QAAQ,CAAC,IAAI,CAAC,YAAY,CAAC,kBAAkB,CAAC,aAAa,EAAE,CAAC,CAAC,EAAE,IAAI,CAAC,UAAU,CAAC,cAAc,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAC5H,CAAC;IAED,yFAAyF;IACjF,YAAY,CAAC,aAAqB,EAAE,YAAoB;QAC5D,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,GAAG,GAAG,GAAG,gCAAgC,GAAG,GAAG,GAAG,IAAI,CAAC,CAAC;QACjF,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC,UAAU,EAAE,IAAI,EAAE,YAAY,CAAC,EAAE,gCAAgC,CAAC,CAAC;QAC/F,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC,UAAU,EAAE,aAAa,CAAC,EAAE,oCAAoC,CAAC,CAAC;QAC9F,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,qBAAqB,YAAY,MAAM,CAAC,CAAC;IAClE,CAAC;IAEO,mBAAmB,CACvB,eAAyB,EAAE,QAAgB,EAAE,SAAiB,EAAE,WAAmB,EAAE,QAAgB;QAErG,KAAK,MAAM,IAAI,IAAI,eAAe,EAAE,CAAC;YACjC,MAAM,OAAO,GAAG,IAAI,CAAC,UAAU,CAAC,iBAAiB,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;YAClE,EAAE,CAAC,SAAS,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YAE3C,MAAM,IAAI,GAAG,IAAA,yBAAS,EAAC,KAAK,EAAE,CAAC,MAAM,EAAE,GAAG,SAAS,IAAI,IAAI,EAAE,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC;YACtF,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,iBAAiB,CAAC,EAAE,IAAI,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,wBAAwB,CAAC,CAAC;YAC5H,MAAM,OAAO,GAAG,IAAA,yBAAS,EAAC,KAAK,EAAE,CAAC,MAAM,EAAE,GAAG,WAAW,IAAI,IAAI,EAAE,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC;YAC3F,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,eAAe,CAAC,EAAE,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,wBAAwB,CAAC,CAAC;YAChI,MAAM,IAAI,GAAG,IAAA,yBAAS,EAAC,KAAK,EAAE,CAAC,MAAM,EAAE,GAAG,QAAQ,IAAI,IAAI,EAAE,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC;YACrF,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,YAAY,CAAC,EAAE,IAAI,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,wBAAwB,CAAC,CAAC;YAEvH,MAAM,EAAE,GAAG,IAAA,yBAAS,EAAC,KAAK,EAAE,CAAC,MAAM,EAAE,SAAS,EAAE,WAAW,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC;YAChG,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,UAAU,CAAC,EAAE,EAAE,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC;YAClE,MAAM,EAAE,GAAG,IAAA,yBAAS,EAAC,KAAK,EAAE,CAAC,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC;YAC7F,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,UAAU,CAAC,EAAE,EAAE,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC;QACtE,CAAC;IACL,CAAC;IAED,gGAAgG;IAChG,iGAAiG;IACjG,6FAA6F;IACrF,eAAe,CAAC,QAAgB,EAAE,YAAoB,EAAE,eAAyB,EAAE,aAAqB;QAC5G,MAAM,QAAQ,GAAG,eAAe,CAAC,GAAG,CAAC,CAAC,CAAS,EAAU,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACrF,MAAM,GAAG,GAAG,IAAI,uBAAQ,CACpB,IAAI,+BAAgB,EAAE,CAAC,WAAW,CAAC,aAAa,CAAC,EAAE,aAAa,EAAE,YAAY,EAAE,QAAQ,EAAE,QAAQ,CACrG,CAAC;QACF,OAAO,IAAI,+BAAgB,CAAC,IAAA,2BAAY,EAAC,gCAAiB,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAC7E,CAAC;IAED,gDAAgD;IACxC,oBAAoB,CAAC,QAAgB,EAAE,QAAgB,EAAE,YAAoB,EAAE,eAAyB,EAAE,aAAqB;QACnI,MAAM,MAAM,GAAG,2BAAY,CAAC,SAAS,CAAC,QAAQ,EAAE,aAAa,CAAC,CAAC;QAC/D,EAAE,CAAC,SAAS,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC1C,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,gCAAiB,CAAC,CAAC;QACrD,EAAE,CAAC,aAAa,CAAC,OAAO,EAAE,IAAI,CAAC,eAAe,CAAC,QAAQ,EAAE,YAAY,EAAE,eAAe,EAAE,aAAa,CAAC,CAAC,CAAC;QACxG,OAAO,OAAO,CAAC;IACnB,CAAC;IAED,mFAAmF;IAC3E,qBAAqB,CACzB,OAAe,EAAE,QAAgB,EAAE,YAAoB,EAAE,eAAyB,EAAE,aAAqB;QAEzG,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,GAAG,GAAG,GAAG,oBAAoB,eAAe,CAAC,MAAM,0CAA0C,GAAG,GAAG,GAAG,IAAI,CAAC,CAAC;QACrI,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,yDAAyD,CAAC,CAAC;QAChF,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC;QACtC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,+DAA+D,CAAC,CAAC;QACtF,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,gEAAgE,CAAC,CAAC;QACvF,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,8DAA8D,YAAY,MAAM,CAAC,CAAC;QACvG,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,wBAAwB,CAAC,CAAC;QAC/C,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,sCAAsC,OAAO,IAAI,CAAC,CAAC;QACxE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,sFAAsF,CAAC,CAAC;QAC7G,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,SAAS,QAAQ,oFAAoF,CAAC,CAAC;QAC5H,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,mBAAmB,aAAa,gFAAgF,CAAC,CAAC;QACvI,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,qBAAqB,CAAC,CAAC;QAC5C,KAAK,MAAM,IAAI,IAAI,eAAe;YAAE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,IAAI,IAAI,CAAC,CAAC;QAC1E,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,GAAG,GAAG,CAAC,CAAC;IACrC,CAAC;IAED,8FAA8F;IACtF,uBAAuB,CAC3B,QAAgB,EAAE,QAAgB,EAAE,aAAqB,EAAE,YAAoB,EAC/E,YAAoB,EAAE,QAAgB,EAAE,MAAkB,EAAE,aAAqB;QAEjF,MAAM,GAAG,GAAG,IAAA,wBAAQ,EAAC,sCAAsC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QAC1F,MAAM,eAAe,GAAG,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,CAAS,EAAW,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QACxF,EAAE,CAAC,SAAS,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC5C,gGAAgG;QAChG,iGAAiG;QACjG,+FAA+F;QAC/F,IAAI,CAAC,UAAU,CAAC,cAAc,CAAC,QAAQ,EAAE,eAAe,CAAC,CAAC;QAC1D,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,wBAAwB,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;QACxG,IAAI,CAAC,mBAAmB,CAAC,eAAe,EAAE,QAAQ,EAAE,MAAM,CAAC,aAAa,EAAE,MAAM,CAAC,eAAe,EAAE,MAAM,CAAC,YAAY,CAAC,CAAC;QAEvH,MAAM,MAAM,GAAG,IAAI,yBAAW,CAC1B,aAAa,EAAE,YAAY,EAAE,YAAY,EAAE,QAAQ,EAAE,eAAe,EACpE,MAAM,CAAC,aAAa,EAAE,MAAM,CAAC,eAAe,EAAE,MAAM,CAAC,YAAY,EAAE,KAAK,CAC3E,CAAC;QACF,IAAI,CAAC,UAAU,CAAC,gBAAgB,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QACnD,MAAM,OAAO,GAAG,IAAI,CAAC,oBAAoB,CAAC,QAAQ,EAAE,QAAQ,EAAE,YAAY,EAAE,eAAe,EAAE,aAAa,CAAC,CAAC;QAC5G,IAAI,CAAC,qBAAqB,CAAC,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,eAAe,EAAE,aAAa,CAAC,CAAC;IAChG,CAAC;IAED,6DAA6D;IACrD,QAAQ,CAAC,GAAW;QACxB,MAAM,MAAM,GAAG,IAAA,yBAAS,EAAC,KAAK,EAAE,CAAC,WAAW,EAAE,SAAS,EAAE,GAAG,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC;QACrF,OAAO,MAAM,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IACnE,CAAC;IAED,oGAAoG;IACpG,oGAAoG;IACpG,qBAAqB;IACb,OAAO,CAAC,GAAW;QACvB,MAAM,MAAM,GAAG,IAAA,yBAAS,EAAC,KAAK,EAAE,CAAC,WAAW,EAAE,GAAG,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC;QAC1E,OAAO,MAAM,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IACnE,CAAC;IAED,0FAA0F;IAClF,WAAW,CAAC,QAAgB,EAAE,IAAkB,EAAE,QAAgB;QACtE,MAAM,GAAG,GAAG,IAAA,yBAAS,EAAC,KAAK,EAAE,CAAC,MAAM,EAAE,aAAa,EAAE,iBAAiB,CAAC,EAAE,EAAE,GAAG,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC;QAC9G,MAAM,KAAK,GAAG,CAAC,GAAG,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAS,EAAU,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAS,EAAW,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;QACrJ,MAAM,KAAK,GAAG,IAAI,kCAAmB,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;QACxD,KAAK,CAAC,QAAQ,GAAG,IAAI,CAAC;QACtB,KAAK,CAAC,aAAa,GAAG,KAAK,CAAC;QAC5B,KAAK,CAAC,SAAS,GAAG;YACd,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,mBAAmB,CAAC;YACxC,2BAAY,CAAC,SAAS,CAAC,QAAQ,EAAE,aAAa,EAAE,gCAAiB,CAAC;SACrE,CAAC;QACF,IAAA,gCAAiB,EAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;IACvC,CAAC;CACJ,CAAA;AAzNY,gCAAU;qBAAV,UAAU;IADtB,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;6CAGJ,2BAAU;QACR,4BAAY;QACjB,kBAAO;QACJ,wBAAU;GALlC,UAAU,CAyNtB","sourcesContent":["import { execSync, spawnSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport {\n dotWebpieces, CliExitError,\n MutationVerb, BranchMutationEvent, logBranchMutation, SyncFlowGuidance,\n MERGE_PROCESS_DOC, MergeProcessText, MergeRun, loadTemplate,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { GatherInfo } from '../git-gatherInfo';\nimport { BranchNaming } from './branch-naming';\nimport { GitExec } from './git-exec';\nimport { MergeState, MergeMarker } from './merge-state';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\ninterface HashPoints {\n hashForkPoint: string;\n hashFeatureHead: string;\n hashMainHead: string;\n}\n\n// The four branch names merge-END needs to finalize a merge (swap squash→feature, push, clean up).\nexport class MergeContext {\n currentBranch: string;\n squashBranch: string;\n backupBranch: string;\n prNumber: string;\n\n constructor(currentBranch: string, squashBranch: string, backupBranch: string, prNumber: string) {\n this.currentBranch = currentBranch;\n this.squashBranch = squashBranch;\n this.backupBranch = backupBranch;\n this.prNumber = prNumber;\n }\n}\n\n// Outcome of merge-start: 'clean' carries the context for merge-END to finalize; 'conflict' means the\n// marker + context files were written and the caller should hand back to the AI (exit 2).\nexport class MergeStartResult {\n status: 'clean' | 'conflict';\n context: MergeContext | null;\n runDir: string; // this sync's numbered `merge-<n>/` dir — passed to merge-END so it reads THIS marker\n\n constructor(status: 'clean' | 'conflict', context: MergeContext | null, runDir: string) {\n this.status = status;\n this.context = context;\n this.runDir = runDir;\n }\n}\n\n// The one number `n` for a sync and the things it names: the pre-merge backup branch, its paired\n// conflict-context run dir, and (via `n`) the `preMerge<n>.hash` record of the tip it snapshotted.\nclass SyncSlot {\n backupBranch: string;\n runDir: string;\n n: number;\n\n constructor(backupBranch: string, runDir: string, n: number) {\n this.backupBranch = backupBranch;\n this.runDir = runDir;\n this.n = n;\n }\n}\n\n\n// merge-START: the first half of the 3-point squash-merge lifecycle. Brings origin/main into a fresh\n// `<branch>Squash`, and on conflict writes the 3-point context + unvalidated marker + process doc,\n// then hands control back to the AI. On a clean merge it commits the squash and returns the branch\n// context so the caller (RunUpdate) can run merge-END to finalize. NEVER finalizes or posts a PR.\n@injectable(bindingScopeValues.Singleton)\nexport class MergeStart {\n constructor(\n private readonly gatherInfo: GatherInfo,\n private readonly branchNaming: BranchNaming,\n private readonly gitExec: GitExec,\n private readonly mergeState: MergeState,\n ) {}\n\n async mergeStart(repoRoot: string, verb: MutationVerb, home: string, finishCommand: string): Promise<MergeStartResult> {\n const currentBranch = execSync('git branch --show-current', { encoding: 'utf8' }).trim();\n if (currentBranch.endsWith('Squash')) {\n throw new CliExitError(1, `❌ On a leftover ${currentBranch} branch with no merge marker. Clean up: git branch -D ${currentBranch}`);\n }\n\n // One number `n` for this sync drives BOTH the backup branch and its `merge-<n>/` context dir.\n const slot = this.chooseSyncSlot(home, currentBranch);\n const backupBranch = slot.backupBranch;\n const mergeDir = slot.runDir;\n\n process.stdout.write('\\n' + SEP + '🔄 Squash-Merge Update from Main\\n' + SEP + '\\n');\n const info = await this.gatherInfo.gatherInfo();\n const hashes = info.hashes;\n if (info.alreadyUpToDate) {\n process.stdout.write('ℹ️ Branch already even with main; nothing to merge, continuing to push/build.\\n');\n }\n\n const prNumber = this.detectPr(this.branchNaming.baseBranchName(currentBranch));\n process.stdout.write(prNumber ? `Existing PR #${prNumber} will be updated.\\n` : 'No existing PR (one can be created later).\\n');\n\n this.createBackup(currentBranch, backupBranch);\n // Record THIS sync's pre-merge tip as `staged/<feature>/preMerge<n>.hash` — every intermediate\n // state, not just the last one. It is what lets the `<feature>PreMerge<n>` snapshot BRANCH be\n // disposable (branches count toward the branch cap; a written-down hash does not), and it is the\n // ref the archive tag is cut from when the PR lands.\n this.mergeState.writePreMergeHash(home, slot.n, this.fullSha(currentBranch));\n const backupEvent = new BranchMutationEvent(verb, 'BACKUP');\n backupEvent.fromBranch = currentBranch;\n backupEvent.toBranch = backupBranch;\n logBranchMutation(repoRoot, backupEvent);\n\n const squashBranch = `${currentBranch}Squash`;\n if (spawnSync('git', ['show-ref', '--verify', '--quiet', `refs/heads/${squashBranch}`]).status === 0) {\n throw new CliExitError(1, `❌ Stale ${squashBranch} from a previous run. Delete it: git branch -D ${squashBranch}`);\n }\n\n // Branch the squash off origin/main directly — worktree-native. origin/main was already fetched\n // in gatherInfo(), and A/B/C are computed purely from origin/main, so the merge base is identical\n // to the old checkout-main + pull path.\n const originMainSha = this.shortSha('origin/main');\n this.gitExec.runGitChecked(['checkout', '-b', squashBranch, 'origin/main'], 'Failed to create squash branch off origin/main');\n const baseEvent = new BranchMutationEvent(verb, 'PULL');\n baseEvent.newMain = originMainSha;\n logBranchMutation(repoRoot, baseEvent);\n\n process.stdout.write('\\n' + SEP + `🔀 Squash merging ${currentBranch}\\n` + SEP + '\\n');\n const merge = spawnSync('git', ['merge', '--squash', currentBranch], { stdio: 'inherit' });\n if (merge.status !== 0) {\n this.handleConflictsHandback(repoRoot, mergeDir, currentBranch, squashBranch, backupBranch, prNumber, hashes, finishCommand);\n this.logConflict(repoRoot, verb, mergeDir);\n return new MergeStartResult('conflict', null, mergeDir);\n }\n logBranchMutation(repoRoot, new BranchMutationEvent(verb, 'SQUASH'));\n\n const nothingStaged = spawnSync('git', ['diff-index', '--quiet', '--cached', 'HEAD', '--']).status === 0;\n if (nothingStaged) {\n process.stdout.write('ℹ️ Already up-to-date with main (nothing to merge).\\n');\n } else {\n // Internal, transient subject for the single squash commit on the feature branch. It NO LONGER\n // reaches main's history: finish-upsert-pr squash-merges the PR with an explicit\n // `gh pr merge --subject <PR title> --body-file <commit summary>`, so main carries the PR title.\n this.gitExec.runGitChecked(['commit', '-m', `Squash merge of ${currentBranch}`], 'Failed to commit squash merge');\n // Clean merge ⇒ hashes only, no conflicts.md. Its ABSENCE is what marks this merge clean.\n this.mergeState.recordCleanMerge(mergeDir, hashes.hashForkPoint, hashes.hashFeatureHead, hashes.hashMainHead);\n }\n return new MergeStartResult('clean', new MergeContext(currentBranch, squashBranch, backupBranch, prNumber), mergeDir);\n }\n\n // Detect the PR by its STABLE feature branch (a leftover `…wpN` still resolves to the one name the\n // PR lives on).\n private detectPr(baseBranch: string): string {\n const result = spawnSync(\n 'gh', ['pr', 'list', '--head', baseBranch, '--json', 'number', '--jq', '.[0].number'],\n { encoding: 'utf8' },\n );\n return result.status === 0 ? (result.stdout ?? '').trim() : '';\n }\n\n // Pick this sync's slot number MONOTONICALLY from the durable audit dirs, then step past any\n // existing `<branch>PreMerge<n>` branch so the same `n` is free for the paired backup branch.\n private chooseSyncSlot(home: string, currentBranch: string): SyncSlot {\n const branchExists = (name: string): boolean =>\n spawnSync('git', ['show-ref', '--verify', '--quiet', `refs/heads/${name}`]).status === 0;\n let n = this.mergeState.nextMergeSlotNumber(home);\n while (branchExists(this.branchNaming.preMergeBackupName(currentBranch, n))) n += 1;\n return new SyncSlot(this.branchNaming.preMergeBackupName(currentBranch, n), this.mergeState.mergeRunDirFor(home, n), n);\n }\n\n // Snapshot the pre-merge state onto the caller-chosen `backupBranch`, never overwriting.\n private createBackup(currentBranch: string, backupBranch: string): void {\n process.stdout.write('\\n' + SEP + '💾 Creating Pre-Merge Backup\\n' + SEP + '\\n');\n this.gitExec.runGitChecked(['checkout', '-b', backupBranch], 'Failed to create backup branch');\n this.gitExec.runGitChecked(['checkout', currentBranch], 'Failed to return to feature branch');\n process.stdout.write(`✅ Backup created: ${backupBranch}\\n\\n`);\n }\n\n private saveConflictContext(\n conflictedFiles: string[], mergeDir: string, forkPoint: string, featureHead: string, mainHead: string,\n ): void {\n for (const file of conflictedFiles) {\n const fileDir = this.mergeState.perFileContextDir(mergeDir, file);\n fs.mkdirSync(fileDir, { recursive: true });\n\n const fork = spawnSync('git', ['show', `${forkPoint}:${file}`], { encoding: 'utf8' });\n fs.writeFileSync(path.join(fileDir, 'A-forkpoint.txt'), fork.status === 0 ? (fork.stdout ?? '') : '(file did not exist)\\n');\n const feature = spawnSync('git', ['show', `${featureHead}:${file}`], { encoding: 'utf8' });\n fs.writeFileSync(path.join(fileDir, 'B-feature.txt'), feature.status === 0 ? (feature.stdout ?? '') : '(file did not exist)\\n');\n const main = spawnSync('git', ['show', `${mainHead}:${file}`], { encoding: 'utf8' });\n fs.writeFileSync(path.join(fileDir, 'C-main.txt'), main.status === 0 ? (main.stdout ?? '') : '(file did not exist)\\n');\n\n const ba = spawnSync('git', ['diff', forkPoint, featureHead, '--', file], { encoding: 'utf8' });\n fs.writeFileSync(path.join(fileDir, 'B-A.diff'), ba.stdout ?? '');\n const ca = spawnSync('git', ['diff', forkPoint, mainHead, '--', file], { encoding: 'utf8' });\n fs.writeFileSync(path.join(fileDir, 'C-A.diff'), ca.stdout ?? '');\n }\n }\n\n // The LIVE rendering of the same template `.webpieces/instruct-ai/webpieces.mergeprocess.md` is\n // delivered from — see rules-config/src/merge-process-doc.ts. The text lives there, not here, so\n // the copy every repo receives and the copy a conflicted merge stamps are the same document.\n private mergeProcessDoc(mergeDir: string, squashBranch: string, conflictedFiles: string[], finishCommand: string): string {\n const fileList = conflictedFiles.map((f: string): string => `- \\`${f}\\``).join('\\n');\n const run = new MergeRun(\n new SyncFlowGuidance().pairedStart(finishCommand), finishCommand, squashBranch, mergeDir, fileList,\n );\n return new MergeProcessText(loadTemplate(MERGE_PROCESS_DOC)).render(run);\n }\n\n // Returns the absolute path of the written doc.\n private writeMergeProcessDoc(repoRoot: string, mergeDir: string, squashBranch: string, conflictedFiles: string[], finishCommand: string): string {\n const docDir = dotWebpieces.localFile(repoRoot, 'instruct-ai');\n fs.mkdirSync(docDir, { recursive: true });\n const docPath = path.join(docDir, MERGE_PROCESS_DOC);\n fs.writeFileSync(docPath, this.mergeProcessDoc(mergeDir, squashBranch, conflictedFiles, finishCommand));\n return docPath;\n }\n\n // The AI-facing \"what just happened / what to do next\" recap on the conflict path.\n private printConflictHandback(\n docPath: string, mergeDir: string, squashBranch: string, conflictedFiles: string[], finishCommand: string,\n ): void {\n process.stdout.write('\\n' + SEP + `⚠️ Conflicts in ${conflictedFiles.length} file(s) — handing control back to you\\n` + SEP + '\\n');\n process.stdout.write('Here is exactly what I did and what you need to do:\\n\\n');\n process.stdout.write('What I did:\\n');\n process.stdout.write(' 1. snapshotted your pre-merge state to a PreMerge branch\\n');\n process.stdout.write(' 2. pulled origin/main and squash-merged your work onto it\\n');\n process.stdout.write(` 3. hit conflicts — you are now on the transient branch ${squashBranch}\\n\\n`);\n process.stdout.write('What you need to do:\\n');\n process.stdout.write(` 1. read the merge process doc: ${docPath}\\n`);\n process.stdout.write(` 2. resolve each conflicted file below (its 3-point A/B/C context + diffs are in\\n`);\n process.stdout.write(` ${mergeDir}/updatemain-<file>/), \\`git add\\` it, and write that file's merge-explanation.md\\n`);\n process.stdout.write(` 3. run pnpm ${finishCommand} — it validates, commits, and finalizes (do NOT git commit/push yourself)\\n\\n`);\n process.stdout.write('Conflicted files:\\n');\n for (const file of conflictedFiles) process.stdout.write(` - ${file}\\n`);\n process.stdout.write('\\n' + SEP);\n }\n\n // Write the conflict context files + the unvalidated marker + the process doc. Does NOT exit.\n private handleConflictsHandback(\n repoRoot: string, mergeDir: string, currentBranch: string, squashBranch: string,\n backupBranch: string, prNumber: string, hashes: HashPoints, finishCommand: string,\n ): void {\n const raw = execSync('git diff --name-only --diff-filter=U', { encoding: 'utf8' }).trim();\n const conflictedFiles = raw.split('\\n').filter((f: string): boolean => f.trim() !== '');\n fs.mkdirSync(mergeDir, { recursive: true });\n // `conflicts.md` replaces the old `updatemain-conflicted-files.txt` (which nothing read) AND is\n // the 3-point signal itself: this file exists in a run dir if and only if that merge conflicted,\n // which is what lets the index classify each merge without a per-branch directory-name scheme.\n this.mergeState.writeConflicts(mergeDir, conflictedFiles);\n fs.writeFileSync(path.join(mergeDir, 'updatemain-hashes.json'), JSON.stringify(hashes, null, 2) + '\\n');\n this.saveConflictContext(conflictedFiles, mergeDir, hashes.hashForkPoint, hashes.hashFeatureHead, hashes.hashMainHead);\n\n const marker = new MergeMarker(\n currentBranch, squashBranch, backupBranch, prNumber, conflictedFiles,\n hashes.hashForkPoint, hashes.hashFeatureHead, hashes.hashMainHead, false,\n );\n this.mergeState.writeMergeMarker(mergeDir, marker);\n const docPath = this.writeMergeProcessDoc(repoRoot, mergeDir, squashBranch, conflictedFiles, finishCommand);\n this.printConflictHandback(docPath, mergeDir, squashBranch, conflictedFiles, finishCommand);\n }\n\n // Short sha of a ref (best-effort — '' if it can't resolve).\n private shortSha(ref: string): string {\n const result = spawnSync('git', ['rev-parse', '--short', ref], { encoding: 'utf8' });\n return result.status === 0 ? (result.stdout ?? '').trim() : '';\n }\n\n // FULL sha of a ref (best-effort — '' if it can't resolve). Recorded for the pre-merge tips because\n // those hashes are meant to be used later to restore state, and an abbreviated sha can go ambiguous\n // as the repo grows.\n private fullSha(ref: string): string {\n const result = spawnSync('git', ['rev-parse', ref], { encoding: 'utf8' });\n return result.status === 0 ? (result.stdout ?? '').trim() : '';\n }\n\n // Log the CONFLICT phase with the conflicted-file list + artifact paths a resolver needs.\n private logConflict(repoRoot: string, verb: MutationVerb, mergeDir: string): void {\n const raw = spawnSync('git', ['diff', '--name-only', '--diff-filter=U'], { cwd: repoRoot, encoding: 'utf8' });\n const files = (raw.status === 0 ? (raw.stdout ?? '') : '').split('\\n').map((f: string): string => f.trim()).filter((f: string): boolean => f !== '');\n const event = new BranchMutationEvent(verb, 'CONFLICT');\n event.conflict = true;\n event.conflictFiles = files;\n event.artifacts = [\n path.join(mergeDir, 'updatemain-<file>'),\n dotWebpieces.localFile(repoRoot, 'instruct-ai', MERGE_PROCESS_DOC),\n ];\n logBranchMutation(repoRoot, event);\n }\n}\n"]}