sela-core 0.1.0-alpha.31 → 0.1.0-alpha.33

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.
@@ -0,0 +1,16 @@
1
+ import type { Command } from "commander";
2
+ /**
3
+ * `sela pr aggregate --ledger-dir <path>`
4
+ *
5
+ * CI fan-in: after all sharded Playwright runners complete and upload their
6
+ * `.sela/pending-prompts` artifacts, the downstream merge job downloads
7
+ * everything into one directory and calls this command to open a single
8
+ * unified PR for the entire test suite.
9
+ *
10
+ * Typical GHA usage:
11
+ * - Upload: actions/upload-artifact@v4 path: .sela/pending-prompts/
12
+ * - Download: actions/download-artifact@v4 merge-multiple: true path: sela-ledgers/
13
+ * - Run: npx sela pr aggregate --ledger-dir ./sela-ledgers
14
+ */
15
+ export declare function registerPr(parent: Command): void;
16
+ //# sourceMappingURL=pr.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pr.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/pr.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAazC;;;;;;;;;;;;GAYG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAgDhD"}
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.registerPr = registerPr;
37
+ const path = __importStar(require("path"));
38
+ const PendingPromptLedger_js_1 = require("../../services/PendingPromptLedger.js");
39
+ const SelaReporter_js_1 = require("../../reporter/SelaReporter.js");
40
+ const PRAutomationService_js_1 = require("../../services/PRAutomationService.js");
41
+ /**
42
+ * `sela pr aggregate --ledger-dir <path>`
43
+ *
44
+ * CI fan-in: after all sharded Playwright runners complete and upload their
45
+ * `.sela/pending-prompts` artifacts, the downstream merge job downloads
46
+ * everything into one directory and calls this command to open a single
47
+ * unified PR for the entire test suite.
48
+ *
49
+ * Typical GHA usage:
50
+ * - Upload: actions/upload-artifact@v4 path: .sela/pending-prompts/
51
+ * - Download: actions/download-artifact@v4 merge-multiple: true path: sela-ledgers/
52
+ * - Run: npx sela pr aggregate --ledger-dir ./sela-ledgers
53
+ */
54
+ function registerPr(parent) {
55
+ const pr = parent
56
+ .command("pr")
57
+ .description("Pull-request automation commands");
58
+ pr.command("aggregate")
59
+ .description("Aggregate sharded ledger files and open a single unified PR (CI fan-in)")
60
+ .requiredOption("--ledger-dir <path>", "Directory containing all downloaded shard ledger JSON files")
61
+ .option("--cwd <path>", "Repository root used for git operations (defaults to process.cwd())", process.cwd())
62
+ .action(async (opts) => {
63
+ const ledgerDir = path.resolve(opts.cwd, opts.ledgerDir);
64
+ const payloads = (0, PendingPromptLedger_js_1.readGitPayloadsFromDir)(ledgerDir);
65
+ if (payloads.length === 0) {
66
+ console.error(`[sela pr aggregate] No ledger payloads found in ${ledgerDir} — nothing to do.`);
67
+ return;
68
+ }
69
+ const healedEvents = (0, SelaReporter_js_1.deduplicateByStableId)(payloads.flatMap((p) => p.healedEvents));
70
+ if (healedEvents.length === 0) {
71
+ console.error("[sela pr aggregate] No healed events after deduplication — nothing to do.");
72
+ return;
73
+ }
74
+ const protectedEvents = payloads.flatMap((p) => p.protectedEvents);
75
+ await (0, PRAutomationService_js_1.runCentralizedGitFlow)({
76
+ cwd: opts.cwd,
77
+ healedEvents,
78
+ protectedEvents,
79
+ });
80
+ });
81
+ }
package/dist/cli/index.js CHANGED
@@ -54,6 +54,7 @@ const bulk_js_1 = require("./commands/bulk.js");
54
54
  const sync_js_1 = require("./commands/sync.js");
55
55
  const showReport_js_1 = require("./commands/showReport.js");
56
56
  const merge_js_1 = require("./commands/merge.js");
57
+ const pr_js_1 = require("./commands/pr.js");
57
58
  (0, ErrorHandler_js_1.registerGlobalHandlers)();
58
59
  // Safety net: route any stray third-party console.log through the Logger's
59
60
  // verbosity-gated debug channel (stderr) so stdout stays pipe-clean and the
@@ -77,6 +78,8 @@ program
77
78
  (0, showReport_js_1.registerShowReport)(program);
78
79
  // ── sela merge - CI fan-in: aggregate sharded reports into one ────────────────
79
80
  (0, merge_js_1.registerMerge)(program);
81
+ // ── sela pr <subcommand> - pull-request automation ───────────────────────────
82
+ (0, pr_js_1.registerPr)(program);
80
83
  // ── sela dna <subcommand> ─────────────────────────────────────────────────────
81
84
  const dna = new commander_1.Command("dna")
82
85
  .description("DNA management commands")
@@ -1 +1 @@
1
- {"version":3,"file":"SelaEngine.d.ts","sourceRoot":"","sources":["../../src/engine/SelaEngine.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAexC,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAG7C,OAAO,EAAe,QAAQ,EAAkB,MAAM,yBAAyB,CAAC;AA0FhF,qBAAa,UAAU;IACrB,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,eAAe,CAAkB;IACzC,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,mBAAmB,CAAmB;IAC9C,OAAO,CAAC,YAAY,CAAsB;IAC1C,OAAO,CAAC,MAAM,CAAiB;IAC/B,OAAO,CAAC,SAAS,CAA6C;IAC9D,OAAO,CAAC,cAAc,CAAoC;IAC1D,OAAO,CAAC,eAAe,CAAkC;;IAuBnD,YAAY,CAChB,IAAI,EAAE,IAAI,EACV,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IA0DnB,IAAI,CACR,IAAI,EAAE,IAAI,EACV,YAAY,EAAE,MAAM,EACpB,mBAAmB,EAAE,MAAM,EAC3B,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,QAAQ,GAAE,QAAmB,GAC5B,OAAO,CAAC,MAAM,CAAC;IAirBlB,OAAO,CAAC,aAAa;IAYrB,OAAO,CAAC,aAAa;IAerB;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,oBAAoB;IAoB5B;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,2BAA2B;IA4BnC,OAAO,CAAC,yBAAyB;IAgDjC,OAAO,CAAC,kBAAkB;IA8D1B,OAAO,CAAC,oBAAoB;IAyD5B,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI;IAIrD,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAItE,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC;YAgLtB,aAAa;IAsB3B,OAAO,CAAC,0BAA0B;IA+BlC,OAAO,CAAC,WAAW;IA6Bb,wBAAwB,CAC5B,IAAI,EAAE,IAAI,EACV,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM;CAqBnB;AAED,OAAO,EAAE,CAAC"}
1
+ {"version":3,"file":"SelaEngine.d.ts","sourceRoot":"","sources":["../../src/engine/SelaEngine.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAcxC,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAG7C,OAAO,EAAe,QAAQ,EAAkB,MAAM,yBAAyB,CAAC;AAoFhF,qBAAa,UAAU;IACrB,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,eAAe,CAAkB;IACzC,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,mBAAmB,CAAmB;IAC9C,OAAO,CAAC,YAAY,CAAsB;IAC1C,OAAO,CAAC,MAAM,CAAiB;IAC/B,OAAO,CAAC,SAAS,CAA6C;IAC9D,OAAO,CAAC,cAAc,CAAoC;IAC1D,OAAO,CAAC,eAAe,CAAkC;;IAuBnD,YAAY,CAChB,IAAI,EAAE,IAAI,EACV,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IA0DnB,IAAI,CACR,IAAI,EAAE,IAAI,EACV,YAAY,EAAE,MAAM,EACpB,mBAAmB,EAAE,MAAM,EAC3B,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,QAAQ,GAAE,QAAmB,GAC5B,OAAO,CAAC,MAAM,CAAC;IAirBlB,OAAO,CAAC,aAAa;IAYrB,OAAO,CAAC,aAAa;IAerB;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,oBAAoB;IAoB5B;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,2BAA2B;IA4BnC,OAAO,CAAC,yBAAyB;IAgDjC,OAAO,CAAC,kBAAkB;IA8D1B,OAAO,CAAC,oBAAoB;IAyD5B,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI;IAIrD,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAItE,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC;YAoGtB,aAAa;IAsB3B,OAAO,CAAC,0BAA0B;IA+BlC,OAAO,CAAC,WAAW;IA6Bb,wBAAwB,CAC5B,IAAI,EAAE,IAAI,EACV,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM;CAqBnB;AAED,OAAO,EAAE,CAAC"}
@@ -42,7 +42,6 @@ const IsolatedDiff_1 = require("../utils/IsolatedDiff");
42
42
  const logger_1 = require("../utils/logger");
43
43
  const SnapshotService_1 = require("../services/SnapshotService");
44
44
  const DOMUtils_1 = require("../utils/DOMUtils");
45
- const FileLock_1 = require("../utils/FileLock");
46
45
  const SourceUpdater_1 = require("../services/SourceUpdater");
47
46
  const ASTSourceUpdater_1 = require("../services/ASTSourceUpdater");
48
47
  const SafetyGuard_1 = require("../services/SafetyGuard");
@@ -51,7 +50,6 @@ const ConfigLoader_1 = require("../config/ConfigLoader");
51
50
  const DryRunGuard_1 = require("../config/DryRunGuard");
52
51
  const HealBudget_1 = require("../services/HealBudget");
53
52
  const HealReportService_1 = require("../services/HealReportService");
54
- const PRAutomationService_1 = require("../services/PRAutomationService");
55
53
  const PendingPromptLedger_1 = require("../services/PendingPromptLedger");
56
54
  const SelaError_1 = require("../errors/SelaError");
57
55
  const SelectorSanitizer_1 = require("../services/SelectorSanitizer");
@@ -979,70 +977,23 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
979
977
  }
980
978
  }
981
979
  }
982
- // ── Dual-Mode post-run lifecycle ─────────────────────────────────
980
+ // ── Cross-process worker ledger ──────────────────────────────────────
983
981
  //
984
- // CI (process.env.CI truthy) Clean-Room PR automation (existing flow).
985
- // Local DX (CI falsy) Write-and-notify: AST mutations are
986
- // already on disk; the reporter prints a
987
- // diff + vscode:// deeplink + report link
988
- // via printHealReview() (no Y/N prompt).
982
+ // Workers serialise their state to a per-worker JSON file so the master
983
+ // process (SelaReporter.onEnd) can aggregate everything after all workers
984
+ // have exited. The ledger carries:
985
+ // PendingPromptEntry[] for local-DX diff display / vscode deeplinks.
986
+ // WorkerGitPayload — for CI centralized git/PR flow.
989
987
  //
990
- // The local branch deliberately bypasses PR automation because:
991
- // AST fixes are ALREADY persisted to disk (the test passed on them).
992
- // • Local devs want to inspect + cherry-pick, not auto-commit.
993
- if (isCiEnvironment()) {
994
- if (this.config.prAutomation.enabled) {
995
- // SELA-5: serialise PR automation (git stash / checkout / commit /
996
- // push) behind a repo-root lock. CI normally runs one worker per
997
- // sharded machine, so the lock is uncontended; if a machine is
998
- // misconfigured with workers>1 it prevents concurrent git operations
999
- // from corrupting the index / worktree. NOTE: this guards against git
1000
- // corruption only — aggregating heals from multiple workers into a
1001
- // single PR is intentionally out of scope (run one worker per shard).
1002
- const prLock = new FileLock_1.FileLock((0, FileLock_1.lockDirFor)(path.join(process.cwd(), ".sela-pr")), { staleMs: 120_000 });
1003
- try {
1004
- const ran = await prLock.withLock(async () => {
1005
- const workerIdx = process.env["TEST_WORKER_INDEX"];
1006
- if (workerIdx !== undefined && workerIdx !== "0") {
1007
- logger_1.logger.warn("[PR] PR automation under workers>1 is unsupported — " +
1008
- "run one worker per CI shard. Serialising to protect the git " +
1009
- "index; heals from other workers may land in separate commits.");
1010
- }
1011
- const branchInfo = (0, PRAutomationService_1.detectBranch)(process.cwd());
1012
- const decision = (0, PRAutomationService_1.decideEffectiveStrategy)(this.config.prAutomation, branchInfo.branch, healedEvents);
1013
- if (decision.downgradeReason) {
1014
- logger_1.logger.warn(`[PR] Strategy downgraded: '${decision.configured}' → '${decision.effective}' ` +
1015
- `(reason: ${decision.downgradeReason}, ` +
1016
- `minAI: ${fmtPctEngine(decision.minAiConfidence)}, ` +
1017
- `minAuditor: ${fmtPctEngine(decision.minAuditorConfidence)})`);
1018
- }
1019
- else {
1020
- logger_1.logger.debug(`[PR] Effective strategy: '${decision.effective}' on branch '${branchInfo.branch ?? "unknown"}'`);
1021
- }
1022
- const ctx = { cwd: process.cwd(), reportHtmlPath };
1023
- await (0, PRAutomationService_1.execute)(this.config.prAutomation, decision, healedEvents, branchInfo, ctx);
1024
- await (0, PRAutomationService_1.handleBugDetected)(this.config.prAutomation.onBugDetected, protectedEvents, branchInfo, ctx);
1025
- });
1026
- if (ran === undefined) {
1027
- logger_1.logger.warn("[PR] Skipped PR automation — another worker holds the PR lock.");
1028
- }
1029
- }
1030
- catch (err) {
1031
- logger_1.logger.warn(`[PR] PR automation step failed: ${err.message}`);
1032
- }
1033
- }
1034
- }
1035
- else if (healedEvents.length > 0) {
1036
- // Local DX: serialise the mutated snapshots to a cross-process
1037
- // ledger. The SelaReporter drains it from `onEnd()` in the main
1038
- // process and prints a non-blocking summary (diff + vscode://
1039
- // deeplink). Mutations are already on disk - the ledger only
1040
- // exists so the reporter knows what to narrate.
988
+ // Both CI and local paths write the ledger. The master process decides
989
+ // which parts to consume based on its own environment check.
990
+ if (healedEvents.length > 0 || protectedEvents.length > 0) {
1041
991
  try {
1042
992
  (0, PendingPromptLedger_1.writePendingPromptsLedger)({
1043
993
  cwd: process.cwd(),
1044
994
  workspace: WorkspaceSnapshotService_1.sharedWorkspaceSnapshot,
1045
995
  healedEvents,
996
+ protectedEvents,
1046
997
  });
1047
998
  }
1048
999
  catch (err) {
@@ -1,7 +1,7 @@
1
1
  import type { Reporter, TestCase, TestResult, FullConfig, Suite, FullResult } from "@playwright/test/reporter";
2
2
  import { HealReportService } from "../services/HealReportService.js";
3
3
  import type { SelaEngine } from "../engine/SelaEngine.js";
4
- import { type PendingPromptEntry } from "../services/PendingPromptLedger.js";
4
+ import { type PendingPromptEntry, type WorkerGitPayload } from "../services/PendingPromptLedger.js";
5
5
  import { type PrintReviewDeps } from "../services/InteractiveReview.js";
6
6
  export interface ReporterEngine {
7
7
  commitUpdates(): Promise<void>;
@@ -61,6 +61,12 @@ export interface SelaReporterOpts {
61
61
  * polluted by leftover handlers between cases.
62
62
  */
63
63
  installSignalHandlers?: boolean;
64
+ /**
65
+ * Override the centralized git/PR flow executed in CI after all workers
66
+ * finish. Tests stub this to avoid real git ops. Defaults to reading the
67
+ * worker ledger and calling PRAutomationService.runCentralizedGitFlow.
68
+ */
69
+ runCentralizedGitFlow?: (cwd: string) => Promise<void>;
64
70
  }
65
71
  export declare class SelaReporter implements Reporter {
66
72
  private readonly engine;
@@ -75,8 +81,10 @@ export declare class SelaReporter implements Reporter {
75
81
  private readonly isCi;
76
82
  private readonly mergeShards;
77
83
  private readonly shouldInstallSignalHandlers;
84
+ private readonly runCentralizedGitFlowFn;
78
85
  private shutdownHandler;
79
86
  private shuttingDown;
87
+ private _isShardedRun;
80
88
  /** Total tests in the suite, captured at onBegin for the end-of-run summary. */
81
89
  private totalTests;
82
90
  /**
@@ -161,5 +169,13 @@ export declare class SelaReporter implements Reporter {
161
169
  */
162
170
  private extractScreenshotBase64;
163
171
  }
172
+ /**
173
+ * Deduplicate HealedEvents by stableId. When the same selector was healed
174
+ * across multiple retries or workers, the last entry wins — it represents
175
+ * the canonical final state of that mutation.
176
+ *
177
+ * Exported for use by `sela pr aggregate` (cross-shard fan-in).
178
+ */
179
+ export declare function deduplicateByStableId(events: WorkerGitPayload["healedEvents"]): WorkerGitPayload["healedEvents"];
164
180
  export default SelaReporter;
165
181
  //# sourceMappingURL=SelaReporter.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"SelaReporter.d.ts","sourceRoot":"","sources":["../../src/reporter/SelaReporter.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"SelaReporter.d.ts","sourceRoot":"","sources":["../../src/reporter/SelaReporter.ts"],"names":[],"mappings":"AA+BA,OAAO,KAAK,EACV,QAAQ,EACR,QAAQ,EACR,UAAU,EACV,UAAU,EACV,KAAK,EACL,UAAU,EACX,MAAM,2BAA2B,CAAC;AAEnC,OAAO,EAEL,iBAAiB,EAIlB,MAAM,kCAAkC,CAAC;AAG1C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAE1D,OAAO,EAIL,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,EACtB,MAAM,oCAAoC,CAAC;AAK5C,OAAO,EAEL,KAAK,eAAe,EACrB,MAAM,kCAAkC,CAAC;AAS1C,MAAM,WAAW,cAAc;IAC7B,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAChC;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5B,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5B,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9B;AAED,MAAM,WAAW,gBAAgB;IAC/B;;;;OAIG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,uDAAuD;IACvD,MAAM,CAAC,EAAE,cAAc,GAAG,UAAU,CAAC;IACrC,kEAAkE;IAClE,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,mDAAmD;IACnD,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,yFAAyF;IACzF,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,kBAAkB,EAAE,CAAC;IACnD,iCAAiC;IACjC,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC;;;OAGG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,eAAe,KAAK,IAAI,CAAC;IAC9C;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;IACnD;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,OAAO,CAAC;IACrB;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC;;;;OAIG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC;;;;OAIG;IACH,qBAAqB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACxD;AAMD,qBAAa,YAAa,YAAW,QAAQ;IAC3C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiB;IACxC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAoB;IAC3C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiB;IACxC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAU;IAC1C,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAC7B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAwC;IACnE,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAwB;IACpD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAkC;IAC9D,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAiC;IACnE,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAgB;IACrC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAwB;IACpD,OAAO,CAAC,QAAQ,CAAC,2BAA2B,CAAU;IACtD,OAAO,CAAC,QAAQ,CAAC,uBAAuB,CAAiC;IAEzE,OAAO,CAAC,eAAe,CAAmD;IAC1E,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,aAAa,CAAS;IAE9B,gFAAgF;IAChF,OAAO,CAAC,UAAU,CAAK;IAEvB;;;;;;;OAOG;IACH,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAA6B;IAEhE;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAMjB;gBAEI,IAAI,GAAE,gBAAqB;IAqBvC,OAAO,CAAC,OAAO,CAAC,EAAE,UAAU,EAAE,KAAK,CAAC,EAAE,KAAK,GAAG,IAAI;IA6BlD,SAAS,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,GAAG,IAAI;IAmC7C,KAAK,CAAC,OAAO,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IA2GhD,yEAAyE;IACzE,aAAa,IAAI,OAAO;IAQxB;;;;;OAKG;IACH,OAAO,CAAC,iBAAiB;IAiBzB;;;;OAIG;IACH,OAAO,CAAC,gBAAgB;IAUxB;;;;;OAKG;IACH,OAAO,CAAC,sBAAsB;IA2B9B,OAAO,CAAC,wBAAwB;IAOhC;;;;;;;OAOG;IACG,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC;IAgCvC;;;;;;;;OAQG;IACH,uBAAuB,IAAI,MAAM;IAsBjC,2DAA2D;IAC3D,mBAAmB,IAAI,aAAa,CAAC;QACnC,UAAU,EAAE,MAAM,CAAC;QACnB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,SAAS,EAAE,MAAM,CAAC;QAClB,MAAM,EAAE,MAAM,CAAC;QACf,aAAa,EAAE,OAAO,CAAC;KACxB,CAAC;IAIF,yEAAyE;IACzE,yBAAyB,IAAI,MAAM;IAQnC,OAAO,CAAC,UAAU;IAIlB,OAAO,CAAC,aAAa;IAWrB;;;;;;OAMG;IACH,OAAO,CAAC,uBAAuB;CA6BhC;AAoED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,gBAAgB,CAAC,cAAc,CAAC,GACvC,gBAAgB,CAAC,cAAc,CAAC,CAMlC;AAID,eAAe,YAAY,CAAC"}
@@ -62,6 +62,7 @@ var __importStar = (this && this.__importStar) || (function () {
62
62
  })();
63
63
  Object.defineProperty(exports, "__esModule", { value: true });
64
64
  exports.SelaReporter = void 0;
65
+ exports.deduplicateByStableId = deduplicateByStableId;
65
66
  const fs = __importStar(require("fs"));
66
67
  const path = __importStar(require("path"));
67
68
  const HealReportService_js_1 = require("../services/HealReportService.js");
@@ -69,6 +70,7 @@ const ReportMergeService_js_1 = require("../services/ReportMergeService.js");
69
70
  const singleton_js_1 = require("../engine/singleton.js");
70
71
  const SelaError_js_1 = require("../errors/SelaError.js");
71
72
  const PendingPromptLedger_js_1 = require("../services/PendingPromptLedger.js");
73
+ const PRAutomationService_js_1 = require("../services/PRAutomationService.js");
72
74
  const InteractiveReview_js_1 = require("../services/InteractiveReview.js");
73
75
  const logger_js_1 = require("../utils/logger.js");
74
76
  const SummaryTable_js_1 = require("../utils/SummaryTable.js");
@@ -88,8 +90,10 @@ class SelaReporter {
88
90
  isCi;
89
91
  mergeShards;
90
92
  shouldInstallSignalHandlers;
93
+ runCentralizedGitFlowFn;
91
94
  shutdownHandler = null;
92
95
  shuttingDown = false;
96
+ _isShardedRun = false;
93
97
  /** Total tests in the suite, captured at onBegin for the end-of-run summary. */
94
98
  totalTests = 0;
95
99
  /**
@@ -120,6 +124,8 @@ class SelaReporter {
120
124
  this.isCi = opts.isCi ?? defaultIsCi;
121
125
  this.mergeShards = opts.mergeShards ?? defaultMergeShards;
122
126
  this.shouldInstallSignalHandlers = opts.installSignalHandlers ?? true;
127
+ this.runCentralizedGitFlowFn =
128
+ opts.runCentralizedGitFlow ?? defaultRunCentralizedGitFlow;
123
129
  }
124
130
  // ─────────────────────────────────────────────────────────────
125
131
  // Playwright Reporter API
@@ -129,6 +135,11 @@ class SelaReporter {
129
135
  // finalisation. Today the fixture still calls engine.commitUpdates()
130
136
  // itself; the env flag is reserved for the planned dedup guard.
131
137
  process.env.SELA_REPORTER_ACTIVE = "1";
138
+ // Detect Playwright CI sharding (--shard=N/M). When sharded, each runner
139
+ // must skip the Git/PR flow and let the downstream fan-in job handle it.
140
+ if (_config?.shard != null) {
141
+ this._isShardedRun = true;
142
+ }
132
143
  // Snapshot the suite size for the end-of-run summary table. Guarded:
133
144
  // allTests() exists on real Playwright suites but not on test stubs.
134
145
  try {
@@ -218,6 +229,25 @@ class SelaReporter {
218
229
  this.logger.error(`[Sela] shard merge failed: ${msg}`);
219
230
  }
220
231
  }
232
+ // ── CI: centralized git/PR flow ──────────────────────────────
233
+ //
234
+ // All workers have exited by the time onEnd() fires. Their git-flow
235
+ // payloads are aggregated from the per-worker ledger files and fed
236
+ // into a single runCentralizedGitFlow call — no lock needed, no race
237
+ // on the git index, no "another worker holds the PR lock" warning.
238
+ //
239
+ // Sharded CI runs (--shard=N/M) skip this entirely: each shard runner
240
+ // only flushes its ledger; the downstream `sela pr aggregate` fan-in
241
+ // job opens the single unified PR after all shards complete.
242
+ if (this.isCi() && !this._isShardedRun) {
243
+ try {
244
+ await this.runCentralizedGitFlowFn(this.cwd);
245
+ }
246
+ catch (err) {
247
+ const msg = err instanceof Error ? err.message : String(err);
248
+ this.logger.error(`[Sela] centralized git flow failed: ${msg}`);
249
+ }
250
+ }
221
251
  // ── Local DX - Write-and-notify summary ─────────────────────
222
252
  //
223
253
  // Workers serialise their mutated FileSnapshots to
@@ -226,8 +256,7 @@ class SelaReporter {
226
256
  // deeplink + HTML report deeplink). No Y/N prompt - the files are
227
257
  // already mutated on disk by the time we get here.
228
258
  //
229
- // CI runs skip the notify entirely - PR automation already handled
230
- // the mutations on its isolated branch.
259
+ // CI runs skip the notify entirely - PR automation is handled above.
231
260
  if (!this.isCi()) {
232
261
  try {
233
262
  const entries = this.readLedger(this.cwd);
@@ -249,9 +278,11 @@ class SelaReporter {
249
278
  }
250
279
  }
251
280
  }
252
- else {
253
- // CI path: drop the ledger so a follow-up local run doesn't replay
254
- // mutations from a previous CI session.
281
+ else if (!this._isShardedRun) {
282
+ // Non-sharded CI: drop the ledger so a follow-up local run doesn't
283
+ // replay mutations from a previous CI session.
284
+ // Sharded runs must NOT clear here — the ledger files must survive
285
+ // until actions/upload-artifact captures them for the fan-in job.
255
286
  try {
256
287
  this.clearLedger(this.cwd);
257
288
  }
@@ -512,6 +543,39 @@ function defaultMergeShards(cwd) {
512
543
  (0, HealReportService_js_1.clearReportShards)(cwd);
513
544
  }
514
545
  }
546
+ /**
547
+ * Default centralized git flow: aggregate all workers' git payloads from the
548
+ * pending-prompts ledger, deduplicate healed events by stableId (last retry
549
+ * wins), then delegate to PRAutomationService.runCentralizedGitFlow.
550
+ */
551
+ async function defaultRunCentralizedGitFlow(cwd) {
552
+ const payloads = (0, PendingPromptLedger_js_1.readGitPayloads)(cwd);
553
+ if (payloads.length === 0)
554
+ return;
555
+ const allHealedEvents = deduplicateByStableId(payloads.flatMap((p) => p.healedEvents));
556
+ const allProtectedEvents = payloads.flatMap((p) => p.protectedEvents);
557
+ if (allHealedEvents.length === 0)
558
+ return;
559
+ await (0, PRAutomationService_js_1.runCentralizedGitFlow)({
560
+ cwd,
561
+ healedEvents: allHealedEvents,
562
+ protectedEvents: allProtectedEvents,
563
+ });
564
+ }
565
+ /**
566
+ * Deduplicate HealedEvents by stableId. When the same selector was healed
567
+ * across multiple retries or workers, the last entry wins — it represents
568
+ * the canonical final state of that mutation.
569
+ *
570
+ * Exported for use by `sela pr aggregate` (cross-shard fan-in).
571
+ */
572
+ function deduplicateByStableId(events) {
573
+ const seen = new Map();
574
+ for (const ev of events) {
575
+ seen.set(ev.stableId, ev);
576
+ }
577
+ return [...seen.values()];
578
+ }
515
579
  // Default export so Playwright can resolve `reporter: [['sela-core/reporter']]`
516
580
  // without needing the consumer to spell out the named import.
517
581
  exports.default = SelaReporter;
@@ -47,4 +47,20 @@ export declare function execute(cfg: ResolvedPRAutomation, decision: StrategyDec
47
47
  /** Injectable for tests - defaults to the module singleton. */
48
48
  workspace?: WorkspaceSnapshotService): Promise<ExecutionResult>;
49
49
  export declare function handleBugDetected(action: BugAction, events: ProtectedEvent[], branchInfo: BranchInfo, ctx: ExecuteContext): Promise<void>;
50
+ export interface CentralizedGitFlowOpts {
51
+ cwd: string;
52
+ healedEvents: HealedEvent[];
53
+ protectedEvents: ProtectedEvent[];
54
+ }
55
+ /**
56
+ * Run the full git/PR automation ONCE in the master process after all
57
+ * Playwright workers have exited. Called from SelaReporter.onEnd().
58
+ *
59
+ * Unlike the old per-worker path this function:
60
+ * - Needs no FileLock (single caller, single process).
61
+ * - Resolves `contentBefore` via `git show HEAD` — retry-proof, never
62
+ * polluted by a prior MUTATE_IN_PLACE run on the same branch.
63
+ * - Accepts pre-aggregated + de-duplicated events from all workers.
64
+ */
65
+ export declare function runCentralizedGitFlow(opts: CentralizedGitFlowOpts): Promise<void>;
50
66
  //# sourceMappingURL=PRAutomationService.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"PRAutomationService.d.ts","sourceRoot":"","sources":["../../src/services/PRAutomationService.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EACV,oBAAoB,EACpB,UAAU,EACV,SAAS,EACV,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACvE,OAAO,EAGL,KAAK,wBAAwB,EAC9B,MAAM,4BAA4B,CAAC;AAQpC,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,MAAM,EAAE,KAAK,GAAG,KAAK,GAAG,MAAM,CAAC;CAChC;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,UAAU,CAAC;IACtB,UAAU,EAAE,UAAU,CAAC;IACvB,eAAe,EAAE,kBAAkB,GAAG,gBAAgB,GAAG,aAAa,GAAG,IAAI,CAAC;IAC9E;;;;OAIG;IACH,eAAe,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,6EAA6E;IAC7E,oBAAoB,EAAE,MAAM,GAAG,SAAS,CAAC;CAC1C;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAiFD,wBAAgB,YAAY,CAAC,GAAG,GAAE,MAAsB,GAAG,UAAU,CA+EpE;AAMD,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,oBAAoB,EACzB,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,KAAK,EAAE,WAAW,EAAE,GACnB,gBAAgB,CAgElB;AAMD,wBAAgB,MAAM,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,CAEzC;AA6FD,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,UAAU,CAAC;IACtB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;;;;;;;;GASG;AACH,wBAAsB,OAAO,CAC3B,GAAG,EAAE,oBAAoB,EACzB,QAAQ,EAAE,gBAAgB,EAC1B,KAAK,EAAE,WAAW,EAAE,EACpB,UAAU,EAAE,UAAU,EACtB,GAAG,EAAE,cAAc;AACnB,+DAA+D;AAC/D,SAAS,GAAE,wBAAkD,GAC5D,OAAO,CAAC,eAAe,CAAC,CAkG1B;AAsTD,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,SAAS,EACjB,MAAM,EAAE,cAAc,EAAE,EACxB,UAAU,EAAE,UAAU,EACtB,GAAG,EAAE,cAAc,GAClB,OAAO,CAAC,IAAI,CAAC,CAgGf"}
1
+ {"version":3,"file":"PRAutomationService.d.ts","sourceRoot":"","sources":["../../src/services/PRAutomationService.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EACV,oBAAoB,EACpB,UAAU,EACV,SAAS,EACV,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACvE,OAAO,EAGL,KAAK,wBAAwB,EAC9B,MAAM,4BAA4B,CAAC;AAQpC,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,MAAM,EAAE,KAAK,GAAG,KAAK,GAAG,MAAM,CAAC;CAChC;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,UAAU,CAAC;IACtB,UAAU,EAAE,UAAU,CAAC;IACvB,eAAe,EAAE,kBAAkB,GAAG,gBAAgB,GAAG,aAAa,GAAG,IAAI,CAAC;IAC9E;;;;OAIG;IACH,eAAe,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,6EAA6E;IAC7E,oBAAoB,EAAE,MAAM,GAAG,SAAS,CAAC;CAC1C;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAiFD,wBAAgB,YAAY,CAAC,GAAG,GAAE,MAAsB,GAAG,UAAU,CA+EpE;AAMD,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,oBAAoB,EACzB,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,KAAK,EAAE,WAAW,EAAE,GACnB,gBAAgB,CAgElB;AAMD,wBAAgB,MAAM,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,CAEzC;AA6FD,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,UAAU,CAAC;IACtB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;;;;;;;;GASG;AACH,wBAAsB,OAAO,CAC3B,GAAG,EAAE,oBAAoB,EACzB,QAAQ,EAAE,gBAAgB,EAC1B,KAAK,EAAE,WAAW,EAAE,EACpB,UAAU,EAAE,UAAU,EACtB,GAAG,EAAE,cAAc;AACnB,+DAA+D;AAC/D,SAAS,GAAE,wBAAkD,GAC5D,OAAO,CAAC,eAAe,CAAC,CAkG1B;AAsTD,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,SAAS,EACjB,MAAM,EAAE,cAAc,EAAE,EACxB,UAAU,EAAE,UAAU,EACtB,GAAG,EAAE,cAAc,GAClB,OAAO,CAAC,IAAI,CAAC,CAgGf;AAMD,MAAM,WAAW,sBAAsB;IACrC,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,EAAE,WAAW,EAAE,CAAC;IAC5B,eAAe,EAAE,cAAc,EAAE,CAAC;CACnC;AAED;;;;;;;;;GASG;AACH,wBAAsB,qBAAqB,CACzC,IAAI,EAAE,sBAAsB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAgDf"}
@@ -51,9 +51,11 @@ exports.decideEffectiveStrategy = decideEffectiveStrategy;
51
51
  exports.fmtPct = fmtPct;
52
52
  exports.execute = execute;
53
53
  exports.handleBugDetected = handleBugDetected;
54
+ exports.runCentralizedGitFlow = runCentralizedGitFlow;
54
55
  const fs = __importStar(require("fs"));
55
56
  const path = __importStar(require("path"));
56
57
  const child_process_1 = require("child_process");
58
+ const ConfigLoader_1 = require("../config/ConfigLoader");
57
59
  const WorkspaceSnapshotService_1 = require("./WorkspaceSnapshotService");
58
60
  const IsolatedDiff_1 = require("../utils/IsolatedDiff");
59
61
  const logger_1 = require("../utils/logger");
@@ -746,6 +748,103 @@ async function handleBugDetected(action, events, branchInfo, ctx) {
746
748
  }
747
749
  }
748
750
  }
751
+ /**
752
+ * Run the full git/PR automation ONCE in the master process after all
753
+ * Playwright workers have exited. Called from SelaReporter.onEnd().
754
+ *
755
+ * Unlike the old per-worker path this function:
756
+ * - Needs no FileLock (single caller, single process).
757
+ * - Resolves `contentBefore` via `git show HEAD` — retry-proof, never
758
+ * polluted by a prior MUTATE_IN_PLACE run on the same branch.
759
+ * - Accepts pre-aggregated + de-duplicated events from all workers.
760
+ */
761
+ async function runCentralizedGitFlow(opts) {
762
+ const cfg = ConfigLoader_1.ConfigLoader.getInstance();
763
+ const prCfg = cfg.prAutomation;
764
+ if (!prCfg.enabled)
765
+ return;
766
+ if (opts.healedEvents.length === 0)
767
+ return;
768
+ const fileSnapshots = resolveGitBaselines(opts.healedEvents, opts.cwd);
769
+ if (fileSnapshots.length === 0) {
770
+ logger_1.logger.warn("[PR] Centralized flow: no mutated files resolved from git baseline — skipping.");
771
+ return;
772
+ }
773
+ const branchInfo = detectBranch(opts.cwd);
774
+ const decision = decideEffectiveStrategy(prCfg, branchInfo.branch, opts.healedEvents);
775
+ if (decision.downgradeReason) {
776
+ logger_1.logger.warn(`[PR] Strategy downgraded: '${decision.configured}' → '${decision.effective}' ` +
777
+ `(reason: ${decision.downgradeReason}, ` +
778
+ `minAI: ${fmtPct(decision.minAiConfidence)}, ` +
779
+ `minAuditor: ${fmtPct(decision.minAuditorConfidence)})`);
780
+ }
781
+ else {
782
+ logger_1.logger.debug(`[PR] Effective strategy: '${decision.effective}' on branch '${branchInfo.branch ?? "unknown"}'`);
783
+ }
784
+ // Minimal workspace shim — execute() only calls getMutatedSnapshots().
785
+ const fakeWorkspace = {
786
+ getMutatedSnapshots: () => fileSnapshots,
787
+ };
788
+ const ctx = { cwd: opts.cwd, reportHtmlPath: null };
789
+ try {
790
+ await execute(prCfg, decision, opts.healedEvents, branchInfo, ctx, fakeWorkspace);
791
+ await handleBugDetected(prCfg.onBugDetected, opts.protectedEvents, branchInfo, ctx);
792
+ }
793
+ catch (err) {
794
+ logger_1.logger.warn(`[PR] Centralized git flow failed: ${err?.message ?? err}`);
795
+ }
796
+ }
797
+ /**
798
+ * Build FileSnapshot[] for the centralized git flow by resolving each
799
+ * mutated file's baseline from `git show HEAD`. This is retry-proof:
800
+ * HEAD always reflects the committed state before any worker's
801
+ * MUTATE_IN_PLACE touched the working tree.
802
+ *
803
+ * contentBefore = headContent = git HEAD content (CI worktree is clean).
804
+ * contentAfter = current file on disk (workers wrote mutations in-place).
805
+ */
806
+ function resolveGitBaselines(heals, cwd) {
807
+ const filePaths = new Set();
808
+ for (const h of heals) {
809
+ const target = h.definitionSite?.file ?? h.sourceFile;
810
+ if (target) {
811
+ filePaths.add(path.isAbsolute(target) ? target : path.resolve(cwd, target));
812
+ }
813
+ }
814
+ const snapshots = [];
815
+ for (const absPath of filePaths) {
816
+ const rel = path.relative(cwd, absPath).replace(/\\/g, "/");
817
+ const gitResult = sh(`git show HEAD:"${rel}"`, cwd);
818
+ const contentAfter = fs.existsSync(absPath)
819
+ ? fs.readFileSync(absPath, "utf8")
820
+ : "";
821
+ if (!gitResult.ok) {
822
+ // File was not tracked at HEAD — created by Sela or untracked.
823
+ snapshots.push({
824
+ absolutePath: absPath,
825
+ relativePath: rel,
826
+ contentBefore: "",
827
+ contentAfter,
828
+ headContent: null,
829
+ hadUncommittedChanges: false,
830
+ createdBySela: true,
831
+ });
832
+ continue;
833
+ }
834
+ const headContent = gitResult.stdout;
835
+ snapshots.push({
836
+ absolutePath: absPath,
837
+ relativePath: rel,
838
+ contentBefore: headContent,
839
+ contentAfter,
840
+ headContent,
841
+ hadUncommittedChanges: false,
842
+ createdBySela: false,
843
+ });
844
+ }
845
+ // Only return files where Sela actually changed something.
846
+ return snapshots.filter((s) => s.contentAfter !== null && s.contentAfter !== s.contentBefore);
847
+ }
749
848
  // ═══════════════════════════════════════════════════════════════════
750
849
  // SHELL QUOTING - cross-platform (POSIX single-quote escape;
751
850
  // on Windows execSync uses cmd.exe which handles double-quotes natively).
@@ -1,5 +1,18 @@
1
1
  import type { WorkspaceSnapshotService } from "./WorkspaceSnapshotService";
2
- import type { HealedEvent } from "./HealReportService";
2
+ import type { HealedEvent, ProtectedEvent } from "./HealReportService";
3
+ /**
4
+ * The subset of worker state that the master process needs to run the
5
+ * centralized git/PR flow in SelaReporter.onEnd(). Written by every worker
6
+ * alongside the local-DX PendingPromptEntry list so the master can aggregate
7
+ * all workers' mutations and fire a single `gh pr create` after every worker
8
+ * has exited — without any FileLock contention.
9
+ */
10
+ export interface WorkerGitPayload {
11
+ /** Absolute paths of every source file this worker mutated. */
12
+ mutatedFiles: string[];
13
+ healedEvents: HealedEvent[];
14
+ protectedEvents: ProtectedEvent[];
15
+ }
3
16
  export interface PendingPromptEntry {
4
17
  absolutePath: string;
5
18
  relativePath: string;
@@ -36,16 +49,34 @@ export declare function writePendingPromptsLedger(opts: {
36
49
  cwd: string;
37
50
  workspace: WorkspaceSnapshotService;
38
51
  healedEvents: HealedEvent[];
52
+ protectedEvents?: ProtectedEvent[];
39
53
  }): string | null;
40
54
  /**
41
55
  * Drain every per-worker JSON file under the ledger directory and merge
42
56
  * their entries into a single de-duplicated list (keyed by absolutePath).
43
57
  * The most-recent contentAfter wins on collision - that's the disk truth.
44
58
  *
59
+ * Handles both v1 (bare PendingPromptEntry[]) and v2 (LedgerFileV2) files.
60
+ *
45
61
  * The directory is NOT deleted here so the reporter can inspect entries
46
62
  * before calling `clearPendingPromptsLedger()`.
47
63
  */
48
64
  export declare function readPendingPromptsLedger(cwd: string): PendingPromptEntry[];
65
+ /**
66
+ * Read the git-flow payloads written by each worker. Used by the master
67
+ * process (SelaReporter.onEnd) to aggregate all workers' mutations and
68
+ * fire a single centralized PR creation after all workers have exited.
69
+ *
70
+ * Only v2 ledger files carry a `git` payload; v1 files are skipped silently.
71
+ */
72
+ export declare function readGitPayloads(cwd: string): WorkerGitPayload[];
73
+ /**
74
+ * Same as readGitPayloads but scans an arbitrary directory directly.
75
+ * Used by `sela pr aggregate --ledger-dir <path>` where the CI fan-in
76
+ * job downloads shard ledger artifacts into a flat directory rather than
77
+ * the standard `.sela/pending-prompts` subpath.
78
+ */
79
+ export declare function readGitPayloadsFromDir(dir: string): WorkerGitPayload[];
49
80
  /** Remove the ledger directory and every file beneath it. Idempotent. */
50
81
  export declare function clearPendingPromptsLedger(cwd: string): void;
51
82
  //# sourceMappingURL=PendingPromptLedger.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"PendingPromptLedger.d.ts","sourceRoot":"","sources":["../../src/services/PendingPromptLedger.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAEV,wBAAwB,EACzB,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAEvD,MAAM,WAAW,kBAAkB;IACjC,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,OAAO,CAAC;IACvB,4EAA4E;IAC5E,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B;;;;;OAKG;IACH,SAAS,EAAE,MAAM,CAAC;CACnB;AAID;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE7C;AAED;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE;IAC9C,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,wBAAwB,CAAC;IACpC,YAAY,EAAE,WAAW,EAAE,CAAC;CAC7B,GAAG,MAAM,GAAG,IAAI,CAehB;AAED;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM,GAAG,kBAAkB,EAAE,CAyB1E;AAED,yEAAyE;AACzE,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAI3D"}
1
+ {"version":3,"file":"PendingPromptLedger.d.ts","sourceRoot":"","sources":["../../src/services/PendingPromptLedger.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAEV,wBAAwB,EACzB,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAMvE;;;;;;GAMG;AACH,MAAM,WAAW,gBAAgB;IAC/B,+DAA+D;IAC/D,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,YAAY,EAAE,WAAW,EAAE,CAAC;IAC5B,eAAe,EAAE,cAAc,EAAE,CAAC;CACnC;AAcD,MAAM,WAAW,kBAAkB;IACjC,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,OAAO,CAAC;IACvB,4EAA4E;IAC5E,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B;;;;;OAKG;IACH,SAAS,EAAE,MAAM,CAAC;CACnB;AAID;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE7C;AAED;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE;IAC9C,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,wBAAwB,CAAC;IACpC,YAAY,EAAE,WAAW,EAAE,CAAC;IAC5B,eAAe,CAAC,EAAE,cAAc,EAAE,CAAC;CACpC,GAAG,MAAM,GAAG,IAAI,CA8BhB;AAED;;;;;;;;;GASG;AACH,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM,GAAG,kBAAkB,EAAE,CAgC1E;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,gBAAgB,EAAE,CAE/D;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,gBAAgB,EAAE,CAmBtE;AAED,yEAAyE;AACzE,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAI3D"}
@@ -55,6 +55,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
55
55
  exports.ledgerDir = ledgerDir;
56
56
  exports.writePendingPromptsLedger = writePendingPromptsLedger;
57
57
  exports.readPendingPromptsLedger = readPendingPromptsLedger;
58
+ exports.readGitPayloads = readGitPayloads;
59
+ exports.readGitPayloadsFromDir = readGitPayloadsFromDir;
58
60
  exports.clearPendingPromptsLedger = clearPendingPromptsLedger;
59
61
  const fs = __importStar(require("fs"));
60
62
  const path = __importStar(require("path"));
@@ -78,15 +80,26 @@ function ledgerDir(cwd) {
78
80
  * empty-mutation runs).
79
81
  */
80
82
  function writePendingPromptsLedger(opts) {
83
+ const protectedEvents = opts.protectedEvents ?? [];
81
84
  const snaps = opts.workspace.getMutatedSnapshots();
82
- if (snaps.length === 0)
83
- return null;
84
85
  const entries = snaps.map((s) => snapshotToEntry(s, opts.healedEvents, opts.cwd));
86
+ const git = {
87
+ mutatedFiles: snaps.map((s) => s.absolutePath),
88
+ healedEvents: opts.healedEvents,
89
+ protectedEvents,
90
+ };
91
+ // Nothing to persist when there are no mutations AND no healed/protected events.
92
+ if (snaps.length === 0 && opts.healedEvents.length === 0 && protectedEvents.length === 0) {
93
+ return null;
94
+ }
85
95
  const dir = ledgerDir(opts.cwd);
86
96
  fs.mkdirSync(dir, { recursive: true });
87
- const fileName = `${process.pid}-${Date.now()}-${randomTag()}.json`;
97
+ // Filename includes workerIndex so concurrent workers never collide.
98
+ const workerIdx = process.env["TEST_WORKER_INDEX"] ?? "0";
99
+ const fileName = `${workerIdx}-${process.pid}-${Date.now()}-${randomTag()}.json`;
88
100
  const full = path.join(dir, fileName);
89
- fs.writeFileSync(full, JSON.stringify(entries, null, 2), "utf8");
101
+ const payload = { version: 2, entries, git };
102
+ fs.writeFileSync(full, JSON.stringify(payload, null, 2), "utf8");
90
103
  return full;
91
104
  }
92
105
  /**
@@ -94,6 +107,8 @@ function writePendingPromptsLedger(opts) {
94
107
  * their entries into a single de-duplicated list (keyed by absolutePath).
95
108
  * The most-recent contentAfter wins on collision - that's the disk truth.
96
109
  *
110
+ * Handles both v1 (bare PendingPromptEntry[]) and v2 (LedgerFileV2) files.
111
+ *
97
112
  * The directory is NOT deleted here so the reporter can inspect entries
98
113
  * before calling `clearPendingPromptsLedger()`.
99
114
  */
@@ -105,18 +120,24 @@ function readPendingPromptsLedger(cwd) {
105
120
  const files = fs
106
121
  .readdirSync(dir)
107
122
  .filter((f) => f.endsWith(".json"))
108
- .sort(); // pid-timestamp prefix gives a stable, time-ordered merge
123
+ .sort(); // workerIdx-pid-timestamp prefix gives a stable, time-ordered merge
109
124
  for (const f of files) {
110
125
  const full = path.join(dir, f);
111
126
  try {
112
127
  const raw = fs.readFileSync(full, "utf8");
113
128
  const parsed = JSON.parse(raw);
114
- if (!Array.isArray(parsed))
115
- continue;
116
- for (const entry of parsed) {
117
- if (!entry || typeof entry.absolutePath !== "string")
129
+ // v2: { version: 2, entries: [...], git: {...} }
130
+ // v1: bare PendingPromptEntry[]
131
+ const entryList = Array.isArray(parsed)
132
+ ? parsed
133
+ : parsed.version === 2
134
+ ? parsed.entries
135
+ : [];
136
+ for (const entry of entryList) {
137
+ const e = entry;
138
+ if (!e || typeof e.absolutePath !== "string")
118
139
  continue;
119
- merged.set(path.normalize(entry.absolutePath), entry);
140
+ merged.set(path.normalize(e.absolutePath), e);
120
141
  }
121
142
  }
122
143
  catch {
@@ -125,6 +146,42 @@ function readPendingPromptsLedger(cwd) {
125
146
  }
126
147
  return Array.from(merged.values());
127
148
  }
149
+ /**
150
+ * Read the git-flow payloads written by each worker. Used by the master
151
+ * process (SelaReporter.onEnd) to aggregate all workers' mutations and
152
+ * fire a single centralized PR creation after all workers have exited.
153
+ *
154
+ * Only v2 ledger files carry a `git` payload; v1 files are skipped silently.
155
+ */
156
+ function readGitPayloads(cwd) {
157
+ return readGitPayloadsFromDir(ledgerDir(cwd));
158
+ }
159
+ /**
160
+ * Same as readGitPayloads but scans an arbitrary directory directly.
161
+ * Used by `sela pr aggregate --ledger-dir <path>` where the CI fan-in
162
+ * job downloads shard ledger artifacts into a flat directory rather than
163
+ * the standard `.sela/pending-prompts` subpath.
164
+ */
165
+ function readGitPayloadsFromDir(dir) {
166
+ if (!fs.existsSync(dir))
167
+ return [];
168
+ const payloads = [];
169
+ const files = fs.readdirSync(dir).filter((f) => f.endsWith(".json"));
170
+ for (const f of files) {
171
+ const full = path.join(dir, f);
172
+ try {
173
+ const raw = fs.readFileSync(full, "utf8");
174
+ const parsed = JSON.parse(raw);
175
+ if (parsed?.version === 2 && parsed.git) {
176
+ payloads.push(parsed.git);
177
+ }
178
+ }
179
+ catch {
180
+ // Corrupt file - skip.
181
+ }
182
+ }
183
+ return payloads;
184
+ }
128
185
  /** Remove the ledger directory and every file beneath it. Idempotent. */
129
186
  function clearPendingPromptsLedger(cwd) {
130
187
  const dir = ledgerDir(cwd);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sela-core",
3
- "version": "0.1.0-alpha.31",
3
+ "version": "0.1.0-alpha.33",
4
4
  "description": "AI self-healing Playwright wrapper - drop-in replacement for @playwright/test",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",