@bridge_gpt/mcp-server 0.2.49 → 0.2.51
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/README.md +25 -8
- package/build/base-ref.js +28 -3
- package/build/claude-review-workflow-drift-probe.js +130 -0
- package/build/claude-review-workflow-drift.js +173 -0
- package/build/claude-review-workflow.js +81 -16
- package/build/commands.generated.js +5 -5
- package/build/conduct-epic/bridge-client.js +115 -1
- package/build/conduct-epic/cli.js +351 -33
- package/build/conduct-epic/cut-protocol.js +51 -0
- package/build/conductor/done-gate.js +25 -3
- package/build/conductor/install-doctor.js +65 -5
- package/build/conductor/latest-check-selector.js +170 -0
- package/build/conductor/local-merge.js +8 -6
- package/build/conductor-bin.js +1 -1
- package/build/{brainstorm-files.js → council-files.js} +15 -15
- package/build/decision-page-schema.js +1 -1
- package/build/docs.generated.js +1 -1
- package/build/doctor.js +352 -4
- package/build/epic-integration-pr.js +280 -0
- package/build/executor/job-runner.js +7 -1
- package/build/executor/merge-job.js +46 -1
- package/build/executor/worktree.js +46 -1
- package/build/index.js +153 -65
- package/build/init.js +9 -2
- package/build/install-bridge.js +60 -2
- package/build/install-reexec.js +47 -9
- package/build/pipelines.generated.js +8 -2
- package/build/plan-epic-conductor-eligibility.js +183 -0
- package/build/plane/cli.js +12 -2
- package/build/plane/manifest.js +25 -1
- package/build/plane/member-roster.js +61 -7
- package/build/plane/preflight.js +24 -9
- package/build/plane/supervisor.js +77 -5
- package/build/plane/types.js +23 -3
- package/build/readme.generated.js +1 -1
- package/build/run-unit-tests-launcher.js +2 -1
- package/build/setup-epic.js +32 -0
- package/build/sfcc/reads-custom-object-def.js +10 -13
- package/build/sfcc/reads-site-preference.js +5 -5
- package/build/sfcc/reads-system-object.js +4 -4
- package/build/sfcc/writes-custom-object-def.js +7 -7
- package/build/sfcc/writes-site-preference.js +4 -3
- package/build/sfcc/writes-system-object.js +7 -6
- package/build/stale-worktree-doctor.js +120 -0
- package/build/start-tickets-prereqs.js +70 -0
- package/build/start-tickets.js +91 -3
- package/build/version.generated.js +3 -2
- package/package.json +6 -3
- package/pipelines/plan-epic.json +5 -0
- package/build/chain-orchestrator.js +0 -1457
- package/build/chain-utils.js +0 -68
- package/build/command-catalog.js +0 -376
- package/build/schedule-run.js +0 -1300
- package/build/schedule-store.js +0 -172
- package/build/scheduled-prompt.js +0 -115
- package/build/scheduler-backends/at-fallback.js +0 -139
- package/build/scheduler-backends/escaping.js +0 -143
- package/build/scheduler-backends/index.js +0 -72
- package/build/scheduler-backends/launchd.js +0 -225
- package/build/scheduler-backends/systemd-user.js +0 -250
- package/build/scheduler-backends/task-scheduler.js +0 -214
- package/build/scheduler-backends/types.js +0 -23
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared, non-throwing helper that ensures a draft epic-integration PR exists
|
|
3
|
+
* (BAPI-951).
|
|
4
|
+
*
|
|
5
|
+
* Local `gh` is used deliberately here: the Bridge/server pull-request seam has
|
|
6
|
+
* no `draft` parameter and cannot distinguish GitHub's "no commits between
|
|
7
|
+
* branches" 422 from a credential or provider failure — both of which this
|
|
8
|
+
* helper must classify without throwing.
|
|
9
|
+
*
|
|
10
|
+
* A draft PR still receives the `conductor-ci / gate` required check (BAPI-949's
|
|
11
|
+
* trigger has no draft exclusion), while `claude-review.yml` excludes drafts
|
|
12
|
+
* until the PR is marked ready. So opening the integration PR as a draft earns
|
|
13
|
+
* the epic branch its required CI immediately, without paying for a paid review
|
|
14
|
+
* nobody can act on until a human decides the epic is ready.
|
|
15
|
+
*/
|
|
16
|
+
import { execFile as nodeExecFile } from "node:child_process";
|
|
17
|
+
/** Bounded read timeout — matches the existing 5s GitHub probe limit (pr-discovery.ts). */
|
|
18
|
+
export const EPIC_INTEGRATION_PR_READ_TIMEOUT_MS = 5_000;
|
|
19
|
+
/** Separate, larger bounded timeout for the two write operations (`pr create`, `pr ready`). */
|
|
20
|
+
export const EPIC_INTEGRATION_PR_WRITE_TIMEOUT_MS = 20_000;
|
|
21
|
+
/** Bounded result count for the existence probe. */
|
|
22
|
+
const LIST_LIMIT = 20;
|
|
23
|
+
const PR_LIST_JSON_FIELDS = "number,headRefName,baseRefName,isDraft";
|
|
24
|
+
/** The production call sites this helper is invoked from. */
|
|
25
|
+
export const EPIC_INTEGRATION_PR_COMMANDS = [
|
|
26
|
+
"setup-epic",
|
|
27
|
+
"conduct-epic init",
|
|
28
|
+
"conduct-epic catch-up",
|
|
29
|
+
"conduct-epic finish",
|
|
30
|
+
"executor merge",
|
|
31
|
+
];
|
|
32
|
+
/** Only the sanctioned safe fields. Never raw command output. */
|
|
33
|
+
export function formatEpicIntegrationPullRequestOutcome(outcome) {
|
|
34
|
+
if (outcome.kind === "already_open" || outcome.kind === "created") {
|
|
35
|
+
return {
|
|
36
|
+
kind: outcome.kind,
|
|
37
|
+
reason: outcome.reason,
|
|
38
|
+
pr_number: outcome.prNumber,
|
|
39
|
+
readiness: outcome.readiness,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
return { kind: outcome.kind, reason: outcome.reason };
|
|
43
|
+
}
|
|
44
|
+
/** A concise, epic-specific PR title. Does not depend on `gh`. */
|
|
45
|
+
export function buildEpicIntegrationPullRequestTitle(epicKey) {
|
|
46
|
+
return `${epicKey}: epic integration branch`;
|
|
47
|
+
}
|
|
48
|
+
/** A non-empty, epic-specific PR body. Does not depend on `gh`. */
|
|
49
|
+
export function buildEpicIntegrationPullRequestBody(epicKey, command) {
|
|
50
|
+
return [
|
|
51
|
+
`\`${command}\` opened this pull request automatically.`,
|
|
52
|
+
"",
|
|
53
|
+
`It exists so the \`conductor-ci / gate\` required check runs for the ${epicKey} ` +
|
|
54
|
+
"epic integration branch (BAPI-949). Draft status postpones the paid Claude " +
|
|
55
|
+
"review until a human marks this pull request ready for review.",
|
|
56
|
+
"",
|
|
57
|
+
"Only a human merges an epic integration branch — the conductor never merges it.",
|
|
58
|
+
].join("\n");
|
|
59
|
+
}
|
|
60
|
+
function isRecord(value) {
|
|
61
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
62
|
+
}
|
|
63
|
+
/** Bounded diagnostics to distinguish "no local gh", "not authenticated", and "inconclusive". */
|
|
64
|
+
async function classifyGhUnavailable(gh, cwd) {
|
|
65
|
+
let version;
|
|
66
|
+
try {
|
|
67
|
+
version = await gh(["--version"], { cwd, timeoutMs: EPIC_INTEGRATION_PR_READ_TIMEOUT_MS });
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return "gh_unavailable";
|
|
71
|
+
}
|
|
72
|
+
if (version.exitCode !== 0)
|
|
73
|
+
return "gh_unavailable";
|
|
74
|
+
let auth;
|
|
75
|
+
try {
|
|
76
|
+
auth = await gh(["auth", "status"], { cwd, timeoutMs: EPIC_INTEGRATION_PR_READ_TIMEOUT_MS });
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return "probe_inconclusive";
|
|
80
|
+
}
|
|
81
|
+
if (auth.exitCode !== 0)
|
|
82
|
+
return "gh_unauthenticated";
|
|
83
|
+
return "probe_inconclusive";
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Existence probe: `gh pr list` constrained by BOTH head and base. Only a
|
|
87
|
+
* successful, valid, empty JSON array is confirmed absence — anything else
|
|
88
|
+
* (non-zero exit, unparseable output, a non-array shape, an item with an
|
|
89
|
+
* invalid number, or a record whose head/base do not match) is `unavailable`,
|
|
90
|
+
* never treated as absence.
|
|
91
|
+
*/
|
|
92
|
+
async function probeMatchingOpenPr(gh, epicBranch, baseBranch, cwd) {
|
|
93
|
+
const args = [
|
|
94
|
+
"pr",
|
|
95
|
+
"list",
|
|
96
|
+
"--state",
|
|
97
|
+
"open",
|
|
98
|
+
"--head",
|
|
99
|
+
epicBranch,
|
|
100
|
+
"--base",
|
|
101
|
+
baseBranch,
|
|
102
|
+
"--json",
|
|
103
|
+
PR_LIST_JSON_FIELDS,
|
|
104
|
+
"--limit",
|
|
105
|
+
String(LIST_LIMIT),
|
|
106
|
+
];
|
|
107
|
+
let result;
|
|
108
|
+
try {
|
|
109
|
+
result = await gh(args, { cwd, timeoutMs: EPIC_INTEGRATION_PR_READ_TIMEOUT_MS });
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return { kind: "unavailable", reason: await classifyGhUnavailable(gh, cwd) };
|
|
113
|
+
}
|
|
114
|
+
if (result.exitCode !== 0) {
|
|
115
|
+
return { kind: "unavailable", reason: await classifyGhUnavailable(gh, cwd) };
|
|
116
|
+
}
|
|
117
|
+
let parsed;
|
|
118
|
+
try {
|
|
119
|
+
parsed = JSON.parse(result.stdout);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return { kind: "unavailable", reason: "probe_malformed" };
|
|
123
|
+
}
|
|
124
|
+
if (!Array.isArray(parsed)) {
|
|
125
|
+
return { kind: "unavailable", reason: "probe_malformed" };
|
|
126
|
+
}
|
|
127
|
+
if (parsed.length === 0) {
|
|
128
|
+
return { kind: "absent" };
|
|
129
|
+
}
|
|
130
|
+
for (const item of parsed) {
|
|
131
|
+
if (!isRecord(item))
|
|
132
|
+
continue;
|
|
133
|
+
const number = item.number;
|
|
134
|
+
const head = item.headRefName;
|
|
135
|
+
const base = item.baseRefName;
|
|
136
|
+
const isDraft = item.isDraft;
|
|
137
|
+
if (typeof number === "number" &&
|
|
138
|
+
Number.isInteger(number) &&
|
|
139
|
+
number > 0 &&
|
|
140
|
+
head === epicBranch &&
|
|
141
|
+
base === baseBranch &&
|
|
142
|
+
typeof isDraft === "boolean") {
|
|
143
|
+
return { kind: "found", number, isDraft };
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// Records exist but none matches both head and base exactly, or the shape is
|
|
147
|
+
// unexpected — never treated as absence.
|
|
148
|
+
return { kind: "unavailable", reason: "probe_malformed" };
|
|
149
|
+
}
|
|
150
|
+
const NO_COMMITS_BETWEEN_PATTERN = /no commits between/i;
|
|
151
|
+
/** Strict positive numeric suffix of a normal `https://…/pull/<n>` URL, else `null`. */
|
|
152
|
+
function extractPrNumberFromCreateOutput(stdout) {
|
|
153
|
+
const match = stdout.trim().match(/\/pull\/(\d+)\s*$/);
|
|
154
|
+
if (!match)
|
|
155
|
+
return null;
|
|
156
|
+
const n = Number(match[1]);
|
|
157
|
+
return Number.isInteger(n) && n > 0 ? n : null;
|
|
158
|
+
}
|
|
159
|
+
async function createDraftPr(gh, epicKey, epicBranch, baseBranch, command, cwd) {
|
|
160
|
+
const title = buildEpicIntegrationPullRequestTitle(epicKey);
|
|
161
|
+
const body = buildEpicIntegrationPullRequestBody(epicKey, command);
|
|
162
|
+
const args = [
|
|
163
|
+
"pr",
|
|
164
|
+
"create",
|
|
165
|
+
"--draft",
|
|
166
|
+
"--base",
|
|
167
|
+
baseBranch,
|
|
168
|
+
"--head",
|
|
169
|
+
epicBranch,
|
|
170
|
+
"--title",
|
|
171
|
+
title,
|
|
172
|
+
"--body",
|
|
173
|
+
body,
|
|
174
|
+
];
|
|
175
|
+
let result;
|
|
176
|
+
try {
|
|
177
|
+
result = await gh(args, { cwd, timeoutMs: EPIC_INTEGRATION_PR_WRITE_TIMEOUT_MS });
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
return { kind: "create_failed" };
|
|
181
|
+
}
|
|
182
|
+
if (result.exitCode === 0) {
|
|
183
|
+
return { kind: "created", numberHint: extractPrNumberFromCreateOutput(result.stdout) };
|
|
184
|
+
}
|
|
185
|
+
const combined = `${result.stdout}\n${result.stderr}`;
|
|
186
|
+
if (NO_COMMITS_BETWEEN_PATTERN.test(combined)) {
|
|
187
|
+
return { kind: "no_commits" };
|
|
188
|
+
}
|
|
189
|
+
return { kind: "create_failed" };
|
|
190
|
+
}
|
|
191
|
+
async function maybeMakeReady(gh, prNumber, isDraft, requestReady, cwd) {
|
|
192
|
+
if (!requestReady)
|
|
193
|
+
return "not_requested";
|
|
194
|
+
if (prNumber === null)
|
|
195
|
+
return "ready_failed";
|
|
196
|
+
if (!isDraft)
|
|
197
|
+
return "already_ready";
|
|
198
|
+
try {
|
|
199
|
+
const result = await gh(["pr", "ready", String(prNumber)], {
|
|
200
|
+
cwd,
|
|
201
|
+
timeoutMs: EPIC_INTEGRATION_PR_WRITE_TIMEOUT_MS,
|
|
202
|
+
});
|
|
203
|
+
return result.exitCode === 0 ? "made_ready" : "ready_failed";
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
return "ready_failed";
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
async function ensureEpicIntegrationPullRequestInner(options) {
|
|
210
|
+
const { epicKey, epicBranch, baseBranch, command, gh, cwd, requestReady } = options;
|
|
211
|
+
const probe = await probeMatchingOpenPr(gh, epicBranch, baseBranch, cwd);
|
|
212
|
+
if (probe.kind === "unavailable") {
|
|
213
|
+
return { kind: "unavailable", reason: probe.reason };
|
|
214
|
+
}
|
|
215
|
+
if (probe.kind === "found") {
|
|
216
|
+
const readiness = await maybeMakeReady(gh, probe.number, probe.isDraft, requestReady, cwd);
|
|
217
|
+
return { kind: "already_open", reason: "already_open", prNumber: probe.number, readiness };
|
|
218
|
+
}
|
|
219
|
+
// Confirmed absent: create.
|
|
220
|
+
const created = await createDraftPr(gh, epicKey, epicBranch, baseBranch, command, cwd);
|
|
221
|
+
if (created.kind === "no_commits") {
|
|
222
|
+
return { kind: "deferred", reason: "no_commits_between_branches" };
|
|
223
|
+
}
|
|
224
|
+
if (created.kind === "created") {
|
|
225
|
+
// Resolve the number through a fresh matching probe first; fall back to the
|
|
226
|
+
// strict URL-derived hint only when the probe cannot confirm it.
|
|
227
|
+
const reprobe = await probeMatchingOpenPr(gh, epicBranch, baseBranch, cwd);
|
|
228
|
+
const prNumber = reprobe.kind === "found" ? reprobe.number : created.numberHint;
|
|
229
|
+
const isDraft = reprobe.kind === "found" ? reprobe.isDraft : true;
|
|
230
|
+
const readiness = await maybeMakeReady(gh, prNumber, isDraft, requestReady, cwd);
|
|
231
|
+
return { kind: "created", reason: "created", prNumber, readiness };
|
|
232
|
+
}
|
|
233
|
+
// Generic create failure: resolve a possible concurrent creator once, never more.
|
|
234
|
+
const race = await probeMatchingOpenPr(gh, epicBranch, baseBranch, cwd);
|
|
235
|
+
if (race.kind === "found") {
|
|
236
|
+
const readiness = await maybeMakeReady(gh, race.number, race.isDraft, requestReady, cwd);
|
|
237
|
+
return { kind: "already_open", reason: "already_open", prNumber: race.number, readiness };
|
|
238
|
+
}
|
|
239
|
+
return { kind: "unavailable", reason: "create_unavailable" };
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Ensure a matching draft epic-integration pull request exists. Idempotent and
|
|
243
|
+
* never throws or rejects — every internal failure, including an injected
|
|
244
|
+
* runner throwing, resolves to `{ kind: "unavailable", ... }`.
|
|
245
|
+
*/
|
|
246
|
+
export async function ensureEpicIntegrationPullRequest(options) {
|
|
247
|
+
try {
|
|
248
|
+
return await ensureEpicIntegrationPullRequestInner(options);
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
return { kind: "unavailable", reason: "probe_inconclusive" };
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Build the production `gh` runner. Uses `execFile` (never a shell, never
|
|
256
|
+
* interpolated argv), ignores stdin, captures stdout/stderr, and bounds both
|
|
257
|
+
* the timeout and the output buffer. Every process-level failure (missing
|
|
258
|
+
* binary, timeout, spawn error) resolves a non-zero/timed-out result rather
|
|
259
|
+
* than rejecting.
|
|
260
|
+
*/
|
|
261
|
+
export function createProductionEpicIntegrationGhRunner(execFileImpl = nodeExecFile) {
|
|
262
|
+
return (args, options) => new Promise((resolve) => {
|
|
263
|
+
execFileImpl("gh", args, {
|
|
264
|
+
cwd: options.cwd,
|
|
265
|
+
timeout: options.timeoutMs,
|
|
266
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
267
|
+
encoding: "utf-8",
|
|
268
|
+
shell: false,
|
|
269
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
270
|
+
}, (error, stdout, stderr) => {
|
|
271
|
+
if (error) {
|
|
272
|
+
const timedOut = error.killed === true;
|
|
273
|
+
const code = typeof error.code === "number" ? error.code : null;
|
|
274
|
+
resolve({ exitCode: code, stdout: stdout ?? "", stderr: stderr ?? "", timedOut });
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
resolve({ exitCode: 0, stdout: stdout ?? "", stderr: stderr ?? "", timedOut: false });
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
}
|
|
@@ -392,7 +392,7 @@ async function checkAdapterMcpAdvisoryForPreparedWorktree(worktreePath, deps, ad
|
|
|
392
392
|
* `controls.signal` (overall-timeout abort) is threaded into local merge deps
|
|
393
393
|
* without changing the local `gh` credential model.
|
|
394
394
|
*/
|
|
395
|
-
export async function defaultRunMergeForClaimed(job, deps,
|
|
395
|
+
export async function defaultRunMergeForClaimed(job, deps, options, controls) {
|
|
396
396
|
const accessResult = await buildConductorMergeAccessForExecutorJob({
|
|
397
397
|
env: deps.env,
|
|
398
398
|
cwd: deps.cwd,
|
|
@@ -419,6 +419,12 @@ export async function defaultRunMergeForClaimed(job, deps, _options, controls) {
|
|
|
419
419
|
// Thread the overall-timeout abort signal into local merge deps WITHOUT
|
|
420
420
|
// changing the local `gh` credential model (the signal is not a credential).
|
|
421
421
|
localMergeDeps: buildDefaultMergeLocalDeps(deps, controls?.signal),
|
|
422
|
+
// BAPI-951: the executor's own configured base — resolveExecutorJobBaseBranch
|
|
423
|
+
// (called inside runExecutorMergeJob) resolves the CHILD PR's base from the
|
|
424
|
+
// job's persisted run base, falling back to this value for a legacy job.
|
|
425
|
+
repositoryBaseBranch: options.baseBranch,
|
|
426
|
+
epicIntegrationAdvisoryLog: deps.errorLog,
|
|
427
|
+
cwd: deps.cwd,
|
|
422
428
|
});
|
|
423
429
|
}
|
|
424
430
|
/** An in-memory owned process for the no-op `smoke` acceptance job. */
|
|
@@ -23,7 +23,11 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import { makeLocalMergeExecutor, resolveLocalMergeMethod, } from "../conductor/local-merge.js";
|
|
25
25
|
import { resolveConductorBridgeApiAccess, } from "../conductor/bridge-api-client.js";
|
|
26
|
+
import { createProductionEpicIntegrationGhRunner, ensureEpicIntegrationPullRequest, formatEpicIntegrationPullRequestOutcome, } from "../epic-integration-pr.js";
|
|
26
27
|
import { secretFreeErrorMessage } from "./job-errors.js";
|
|
28
|
+
import { resolveExecutorJobBaseBranch } from "./base-branch.js";
|
|
29
|
+
/** `epic/<KEY>` prefix a child PR's base branch carries under BAPI-949/BAPI-950. */
|
|
30
|
+
const EPIC_BRANCH_PREFIX = "epic/";
|
|
27
31
|
function positiveIntOrNull(value) {
|
|
28
32
|
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : null;
|
|
29
33
|
}
|
|
@@ -173,6 +177,43 @@ export function buildMergeJobFailure(response) {
|
|
|
173
177
|
classification: "crashed",
|
|
174
178
|
};
|
|
175
179
|
}
|
|
180
|
+
/**
|
|
181
|
+
* Post-merge draft epic-integration-PR retry (BAPI-951). Invoked ONLY after a
|
|
182
|
+
* successful ticket-bound merge whose child PR based on `epic/<EPIC>` — never
|
|
183
|
+
* for a `main`/other-based merge, and never for any failed merge. Wrapped in
|
|
184
|
+
* its own try/catch (the shared helper is already non-throwing, but this
|
|
185
|
+
* guards against a future/injected implementation replacing an already-decided
|
|
186
|
+
* merge outcome). NEVER changes the returned result, the `error_kind`, or the
|
|
187
|
+
* `/complete` payload — only emits a sanitized advisory line.
|
|
188
|
+
*/
|
|
189
|
+
async function tryEnsurePostMergeEpicIntegrationPr(job, seams) {
|
|
190
|
+
if (seams.repositoryBaseBranch === undefined)
|
|
191
|
+
return;
|
|
192
|
+
try {
|
|
193
|
+
const baseResolution = resolveExecutorJobBaseBranch(job, seams.repositoryBaseBranch);
|
|
194
|
+
if (!baseResolution.ok)
|
|
195
|
+
return;
|
|
196
|
+
const childBaseBranch = baseResolution.baseBranch;
|
|
197
|
+
if (!childBaseBranch.startsWith(EPIC_BRANCH_PREFIX))
|
|
198
|
+
return;
|
|
199
|
+
const epicKey = childBaseBranch.slice(EPIC_BRANCH_PREFIX.length);
|
|
200
|
+
if (epicKey.length === 0)
|
|
201
|
+
return;
|
|
202
|
+
const gh = seams.epicIntegrationGh ?? createProductionEpicIntegrationGhRunner();
|
|
203
|
+
const outcome = await ensureEpicIntegrationPullRequest({
|
|
204
|
+
epicKey,
|
|
205
|
+
epicBranch: childBaseBranch,
|
|
206
|
+
baseBranch: seams.repositoryBaseBranch,
|
|
207
|
+
command: "executor merge",
|
|
208
|
+
gh,
|
|
209
|
+
cwd: seams.cwd,
|
|
210
|
+
});
|
|
211
|
+
seams.epicIntegrationAdvisoryLog?.(`epic integration pr: ${JSON.stringify(formatEpicIntegrationPullRequestOutcome(outcome))}`);
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
// Advisory-only: never allowed to affect the already-decided merge outcome.
|
|
215
|
+
}
|
|
216
|
+
}
|
|
176
217
|
/**
|
|
177
218
|
* Run the deterministic merge. NEVER ensures/recreates a worktree and NEVER
|
|
178
219
|
* spawns a worker. Returns a completion result on a succeeded merge, else a
|
|
@@ -213,7 +254,11 @@ export async function runExecutorMergeJob(job, seams) {
|
|
|
213
254
|
};
|
|
214
255
|
}
|
|
215
256
|
if (response.status === "succeeded") {
|
|
216
|
-
|
|
257
|
+
// Build the successful outcome COMPLETELY first — enrichment can only ever
|
|
258
|
+
// add an advisory log line, never alter what is returned.
|
|
259
|
+
const result = buildMergeJobResult(response, fields);
|
|
260
|
+
await tryEnsurePostMergeEpicIntegrationPr(job, seams);
|
|
261
|
+
return { ok: true, result };
|
|
217
262
|
}
|
|
218
263
|
return { ok: false, failure: buildMergeJobFailure(response) };
|
|
219
264
|
}
|
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
* construction or Worktrunk JSON path parsing here. Per-job create failures are
|
|
7
7
|
* returned as structured job failures (→ `/fail`), never thrown.
|
|
8
8
|
*/
|
|
9
|
-
import { fetchAndResolveBaseSha } from "../base-ref.js";
|
|
9
|
+
import { fetchAndResolveBaseSha, probeClaudeReviewWorkflowDrift, resolveRepositoryDefaultBranch } from "../base-ref.js";
|
|
10
|
+
// BAPI-941: the shared drift diagnostic formatter (a leaf module — no cycle).
|
|
11
|
+
import { formatClaudeReviewWorkflowDriftDiagnostic } from "../claude-review-workflow-drift.js";
|
|
10
12
|
import { commandSucceeded } from "../start-tickets-prereqs.js";
|
|
11
13
|
import { createWorktreeForTicket } from "../worktree-core.js";
|
|
12
14
|
import { provisionCommandsForWorktree } from "../command-provisioning.js";
|
|
@@ -27,6 +29,31 @@ export function resolveExecutorBranch(job) {
|
|
|
27
29
|
return { ok: true, branch: `feature/${ticketKey}` };
|
|
28
30
|
return { ok: false, error: "no branch could be resolved (no expected_branch and no ticket_key)" };
|
|
29
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* BAPI-941: emit the per-spawn `claude-review` workflow-drift advisory for an
|
|
34
|
+
* executor worker. Returns `void` and swallows everything — a diagnostic that
|
|
35
|
+
* could abort the run it is describing would be strictly worse than none.
|
|
36
|
+
*
|
|
37
|
+
* Silent when the base is aligned; INFO when the comparison is unavailable;
|
|
38
|
+
* WARNING, naming the stale base and the remedy, when drift is confirmed.
|
|
39
|
+
*/
|
|
40
|
+
async function emitExecutorWorkflowDriftAdvisory(deps, baseBranch) {
|
|
41
|
+
try {
|
|
42
|
+
const probeDeps = { runCommand: deps.runCommand, cwd: deps.cwd };
|
|
43
|
+
const defaultRef = await resolveRepositoryDefaultBranch(probeDeps);
|
|
44
|
+
const classification = await probeClaudeReviewWorkflowDrift(probeDeps, {
|
|
45
|
+
baseRef: baseBranch,
|
|
46
|
+
defaultRef,
|
|
47
|
+
});
|
|
48
|
+
if (classification.state === "aligned")
|
|
49
|
+
return;
|
|
50
|
+
deps.errorLog?.(formatClaudeReviewWorkflowDriftDiagnostic(classification));
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// A thrown probe is itself an unavailable comparison. Stay silent about the
|
|
54
|
+
// exception (it can echo absolute paths) and never let it reach the caller.
|
|
55
|
+
}
|
|
56
|
+
}
|
|
30
57
|
/** Build the lean shared-worktree deps from the executor deps. */
|
|
31
58
|
function toWorktreeCoreDeps(deps) {
|
|
32
59
|
return {
|
|
@@ -183,6 +210,24 @@ export async function ensureExecutorWorktree(job, options, deps, policy = {}) {
|
|
|
183
210
|
};
|
|
184
211
|
}
|
|
185
212
|
const baseSha = resolvedBase.base_sha;
|
|
213
|
+
// BAPI-941: the `claude-review` workflow-drift advisory, emitted once per
|
|
214
|
+
// executor worker spawn — after the base is resolved and before the worktree
|
|
215
|
+
// exists, matching the interactive `start-tickets` choke point.
|
|
216
|
+
//
|
|
217
|
+
// This path matters MORE than the interactive one, not less. A human whose
|
|
218
|
+
// `claude-review` check goes red can merge past it; a conductor-driven epic
|
|
219
|
+
// cannot, so it burns the full 60-minute review ceiling and parks at
|
|
220
|
+
// `needs_human` with `review_verdictless_ceiling_reached` having produced no
|
|
221
|
+
// verdict at any point. The refs were just refreshed by the fetch above, so
|
|
222
|
+
// the comparison is local and costs no extra network round trip.
|
|
223
|
+
//
|
|
224
|
+
// ADVISORY ONLY. It writes to the executor's stderr logger and the spawn
|
|
225
|
+
// proceeds regardless — including when the comparison cannot be completed.
|
|
226
|
+
// It is not a `RunPolicy` key and not a worker-emitted control signal (R14
|
|
227
|
+
// rules 2 and 3 govern the conductor's decision surface; a spawner's warning
|
|
228
|
+
// is neither), and it never contributes to the fail-CLOSED base-resolution
|
|
229
|
+
// decision above.
|
|
230
|
+
await emitExecutorWorkflowDriftAdvisory(deps, options.baseBranch);
|
|
186
231
|
const row = await createWorktreeForTicket(toWorktreeCoreDeps(deps), key, { [key]: branch }, options.worktrunkBinary, baseSha, guardStaleWorktree, {
|
|
187
232
|
alignExistingBranchTo: baseSha,
|
|
188
233
|
verifyHeadMatches: baseSha,
|