@bridge_gpt/mcp-server 0.2.53 → 0.2.55
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 +116 -56
- package/build/agent-guidance.generated.js +3 -0
- package/build/agent-launchers/claude-executor-adapter.js +3 -0
- package/build/agent-launchers/claude.js +3 -3
- package/build/agent-launchers/prompt.js +8 -11
- package/build/agent-notes.js +178 -0
- package/build/agent-registry.js +5 -2
- package/build/agent-utils.js +58 -0
- package/build/agents.generated.js +1 -1
- package/build/base-ref.js +33 -9
- package/build/bounded-wait.js +174 -0
- package/build/codex-skill-adapter.js +55 -0
- package/build/commands.generated.js +6 -5
- package/build/conductor/bridge-api-client.js +235 -14
- package/build/conductor/bring-up-facts.js +187 -0
- package/build/conductor/claude-hook.js +7 -5
- package/build/conductor/cli.js +28 -0
- package/build/conductor/doctor.js +80 -9
- package/build/conductor/epic-implementer-cli.js +1298 -0
- package/build/conductor/epic-runtime.js +134 -98
- package/build/conductor/errors.js +2 -2
- package/build/conductor/git-ci-types.js +1 -1
- package/build/conductor/git-hooks.js +28 -14
- package/build/conductor/install-doctor.js +11 -5
- package/build/conductor/readiness-cli.js +10 -10
- package/build/conductor/readiness-sections.js +58 -9
- package/build/conductor/readiness.js +120 -4
- package/build/conductor/recovery-cli.js +289 -10
- package/build/conductor/recovery-operations.js +125 -2
- package/build/conductor/repair-contract.js +58 -0
- package/build/conductor/run-branch.js +137 -0
- package/build/conductor/store.js +2 -2
- package/build/conductor/supervisor-runtime.js +1 -1
- package/build/conductor/test-run-branch-vectors.js +165 -0
- package/build/conductor-bin.js +2 -139
- package/build/conductor-claude-hook-bin.js +2 -2
- package/build/conductor-claude-hook-removed-stub-bin.js +31 -0
- package/build/conductor-removed-stub-bin.js +30 -0
- package/build/docs.generated.js +1 -1
- package/build/doctor.js +145 -18
- package/build/drive-epic.js +752 -90
- package/build/epic-implementer-bin.js +145 -0
- package/build/epic-implementer-bundle-cli.js +264 -0
- package/build/epic-implementer-claude-hook-bin.js +3 -0
- package/build/epic-integration-pr.js +5 -3
- package/build/executor/claim-scope.js +104 -0
- package/build/executor/cli.js +14 -25
- package/build/executor/env-file-guard.js +82 -3
- package/build/executor/env.js +6 -0
- package/build/executor/interrupted-worktree.js +60 -0
- package/build/executor/job-errors.js +45 -0
- package/build/executor/job-runner.js +334 -9
- package/build/executor/job-types.js +25 -9
- package/build/executor/merge-tree-classifier.js +171 -0
- package/build/executor/reconcile-overlap-governance.js +129 -0
- package/build/executor/reconcile-overlap-job.js +989 -0
- package/build/executor/reconcile-overlap-types.js +14 -0
- package/build/executor/spawn-job-driver.js +1 -0
- package/build/executor/types.js +2 -0
- package/build/executor/worker-finalization.js +25 -2
- package/build/executor/worker-guard-hook.js +15 -7
- package/build/implement-epic/bridge-client.js +773 -0
- package/build/implement-epic/checkpoint-store.js +542 -0
- package/build/implement-epic/cli.js +3158 -0
- package/build/implement-epic/cut-protocol.js +392 -0
- package/build/implement-epic/lock.js +302 -0
- package/build/implement-epic/pr-state.js +286 -0
- package/build/implement-epic/spawn.js +113 -0
- package/build/index.js +657 -481
- package/build/init.js +72 -8
- package/build/install-bridge-conductor.js +5 -5
- package/build/install-bridge.js +403 -70
- package/build/local-artifact-storage.js +130 -0
- package/build/mcp-host-config.js +22 -60
- package/build/mcp-host-entry-adapter.js +18 -0
- package/build/mcp-host-targets.js +1 -21
- package/build/merge-pull-request.js +1 -1
- package/build/pipelines.generated.js +23 -16
- package/build/plan-epic-conductor-eligibility.js +1 -1
- package/build/plane/cli.js +321 -41
- package/build/plane/manifest.js +209 -1
- package/build/plane/member-roster.js +70 -0
- package/build/plane/preflight.js +128 -12
- package/build/plane/shutdown.js +18 -5
- package/build/plane/status.js +35 -1
- package/build/plane/supervisor.js +546 -164
- package/build/plane/types.js +25 -2
- package/build/polling-policy.js +72 -0
- package/build/readiness-check.js +3 -3
- package/build/readme.generated.js +1 -1
- package/build/review-generation.js +219 -0
- package/build/run-unit-tests-launcher.js +6 -1
- package/build/setup-epic.js +567 -38
- package/build/start-tickets-conductor.js +8 -7
- package/build/ticket-key-utils.js +4 -3
- package/build/ticket-review-artifact-gate.js +461 -0
- package/build/upgrade-cli.js +5 -26
- package/build/version.generated.js +3 -3
- package/build/worker-guard-hook-bin.js +1 -1
- package/docs/CONDUCTOR.md +8 -6
- package/docs/install/mcp-tool-integrations.md +23 -1
- package/package.json +5 -3
- package/pipelines/review-ticket.json +17 -4
|
@@ -15,6 +15,7 @@ import { resolveBapiCredentials } from "../credential-store.js";
|
|
|
15
15
|
import { resolveStartTicketsRepoName } from "../start-tickets-repo.js";
|
|
16
16
|
import { normalizePrNumber, normalizeSha } from "./git-ci-types.js";
|
|
17
17
|
import { ConductorValidationError } from "./errors.js";
|
|
18
|
+
import { resolveDeclaredRunBaseBranch } from "./run-branch.js";
|
|
18
19
|
/** Default Bridge API base URL when `BAPI_BASE_URL` is unset. */
|
|
19
20
|
export const CONDUCTOR_DEFAULT_BASE_URL = "https://bridgegpt-api.com";
|
|
20
21
|
/** Default per-request timeout for conductor Bridge API calls. */
|
|
@@ -257,9 +258,26 @@ function redactDiagnosticValues(diagnostics, secrets) {
|
|
|
257
258
|
* values in `headers` (e.g. `X-API-Key`) are additionally redacted from the
|
|
258
259
|
* preview so a body that echoes the credential can never surface it.
|
|
259
260
|
*/
|
|
260
|
-
async function readSanitizedErrorDiagnostics(resp, headers = {}) {
|
|
261
|
+
async function readSanitizedErrorDiagnostics(resp, headers = {}, options = {}) {
|
|
261
262
|
try {
|
|
262
|
-
const
|
|
263
|
+
const parsed = stripWebhookUrlsDeep(await resp.json());
|
|
264
|
+
const diagnostics = extractSanitizedErrorDiagnostics(parsed);
|
|
265
|
+
if (options.readErrorField && !diagnostics.bodyPreview) {
|
|
266
|
+
// BAPI-1154: opt-in only. The route layer's `_http_error` envelope carries
|
|
267
|
+
// its server-authored explanation in `detail.error`, which the shared
|
|
268
|
+
// extractor deliberately does not read (pinned by its own tests). The
|
|
269
|
+
// repair verbs render that explanation to the operator who asked, so they
|
|
270
|
+
// opt in; every other caller's diagnostics are unchanged.
|
|
271
|
+
const detail = parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
272
|
+
? parsed["detail"]
|
|
273
|
+
: undefined;
|
|
274
|
+
const error = detail && typeof detail === "object" && !Array.isArray(detail)
|
|
275
|
+
? detail["error"]
|
|
276
|
+
: undefined;
|
|
277
|
+
if (typeof error === "string" && error.trim()) {
|
|
278
|
+
diagnostics.bodyPreview = boundedErrorPreview(error.trim());
|
|
279
|
+
}
|
|
280
|
+
}
|
|
263
281
|
const secrets = Object.entries(headers)
|
|
264
282
|
.filter(([k]) => /key|authorization|token/i.test(k))
|
|
265
283
|
.map(([, v]) => v);
|
|
@@ -502,8 +520,8 @@ function conductorPostHeaders(access) {
|
|
|
502
520
|
* request body in the error. The timer is always cleared. Shared core for the
|
|
503
521
|
* POST and PATCH wrappers below.
|
|
504
522
|
*/
|
|
505
|
-
async function fetchConductorJsonWithMethodAndTimeout(method, url, headers, body, timeoutMs, fetchImpl) {
|
|
506
|
-
const { body: parsed } = await fetchConductorJsonWithMethodStatusAndTimeout(method, url, headers, body, timeoutMs, fetchImpl);
|
|
523
|
+
async function fetchConductorJsonWithMethodAndTimeout(method, url, headers, body, timeoutMs, fetchImpl, errorOptions = {}) {
|
|
524
|
+
const { body: parsed } = await fetchConductorJsonWithMethodStatusAndTimeout(method, url, headers, body, timeoutMs, fetchImpl, errorOptions);
|
|
507
525
|
return parsed;
|
|
508
526
|
}
|
|
509
527
|
/**
|
|
@@ -517,7 +535,7 @@ async function fetchConductorJsonWithMethodAndTimeout(method, url, headers, body
|
|
|
517
535
|
* that only sees the body has to issue another request to learn which happened.
|
|
518
536
|
* Success statuses only; every non-2xx path still throws exactly as before.
|
|
519
537
|
*/
|
|
520
|
-
async function fetchConductorJsonWithMethodStatusAndTimeout(method, url, headers, body, timeoutMs, fetchImpl) {
|
|
538
|
+
async function fetchConductorJsonWithMethodStatusAndTimeout(method, url, headers, body, timeoutMs, fetchImpl, errorOptions = {}) {
|
|
521
539
|
const controller = new AbortController();
|
|
522
540
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
523
541
|
try {
|
|
@@ -529,7 +547,7 @@ async function fetchConductorJsonWithMethodStatusAndTimeout(method, url, headers
|
|
|
529
547
|
throw new ConductorBridgeApiError(controller.signal.aborted ? "timeout" : "network");
|
|
530
548
|
}
|
|
531
549
|
if (!resp.ok) {
|
|
532
|
-
const diagnostics = await readSanitizedErrorDiagnostics(resp, headers);
|
|
550
|
+
const diagnostics = await readSanitizedErrorDiagnostics(resp, headers, errorOptions);
|
|
533
551
|
if (resp.status === 401 || resp.status === 403) {
|
|
534
552
|
throw new ConductorBridgeApiError("unauthorized", resp.status, diagnostics);
|
|
535
553
|
}
|
|
@@ -860,15 +878,19 @@ export async function readEpicRunCompletionState(access, epicRunId, fetchImpl =
|
|
|
860
878
|
if (!isEpicRunStatus(status)) {
|
|
861
879
|
return { ok: false, reason: "malformed" };
|
|
862
880
|
}
|
|
881
|
+
// BAPI-1127: the declared branch comes from the ONE canonical resolver, not a
|
|
882
|
+
// third inline read. The read this replaced consulted `base_branch` only (so a
|
|
883
|
+
// `baseBranch` row reported no feature branch at all) and assigned the value
|
|
884
|
+
// untrimmed (so `" epic/X "` reached the wind-down PR path with its
|
|
885
|
+
// whitespace intact). `policy_json` is untrusted boundary input and the
|
|
886
|
+
// resolver takes it as `unknown` by design — every non-mapping shape declares
|
|
887
|
+
// nothing rather than throwing.
|
|
863
888
|
const policyJson = epicRun["policy_json"];
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
return { ok: true, state: featureBranch ? { status, featureBranch } : { status } };
|
|
889
|
+
const featureBranch = resolveDeclaredRunBaseBranch(policyJson);
|
|
890
|
+
return {
|
|
891
|
+
ok: true,
|
|
892
|
+
state: featureBranch !== undefined ? { status, featureBranch } : { status },
|
|
893
|
+
};
|
|
872
894
|
}
|
|
873
895
|
/**
|
|
874
896
|
* GET `/jira/epic-runs/runs?repo_name=<repo>&status=active` and return the
|
|
@@ -1160,6 +1182,155 @@ export async function adoptCurrentHeadAndUnparkTicket(access, request, fetchImpl
|
|
|
1160
1182
|
});
|
|
1161
1183
|
return postUnparkLikeRequest(url, conductorPostHeaders(access), body, fetchImpl);
|
|
1162
1184
|
}
|
|
1185
|
+
// ---------------------------------------------------------------------------
|
|
1186
|
+
// BAPI-1154 — run-addressed operator repair verbs (CLI-only; no MCP tool)
|
|
1187
|
+
// ---------------------------------------------------------------------------
|
|
1188
|
+
// Each client sends only its documented body and returns the server's bounded,
|
|
1189
|
+
// authoritative result. None accepts or transmits a head SHA, gate identity,
|
|
1190
|
+
// action key, branch, provider credential, actor, row version, or idempotency
|
|
1191
|
+
// receipt: ownership, identity, and state are the SERVER's to decide. A named
|
|
1192
|
+
// refusal surfaces as a thrown `ConductorBridgeApiError` carrying the server's
|
|
1193
|
+
// `errorCode` (e.g. `TICKET_NOT_IN_RUN`), which the CLI renders by code.
|
|
1194
|
+
function parseRepairNullableString(value) {
|
|
1195
|
+
return typeof value === "string" ? value : null;
|
|
1196
|
+
}
|
|
1197
|
+
function parseRepairBase(parsed) {
|
|
1198
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1199
|
+
throw new ConductorBridgeApiError("server");
|
|
1200
|
+
}
|
|
1201
|
+
const p = parsed;
|
|
1202
|
+
if (typeof p["epic_run_id"] !== "string" ||
|
|
1203
|
+
typeof p["committed"] !== "boolean" ||
|
|
1204
|
+
typeof p["event_recorded"] !== "boolean") {
|
|
1205
|
+
throw new ConductorBridgeApiError("server");
|
|
1206
|
+
}
|
|
1207
|
+
return p;
|
|
1208
|
+
}
|
|
1209
|
+
function parseScopeReparseResult(parsed) {
|
|
1210
|
+
const p = parseRepairBase(parsed);
|
|
1211
|
+
if (typeof p["scope_id"] !== "string" || typeof p["parse_scheduled"] !== "boolean") {
|
|
1212
|
+
throw new ConductorBridgeApiError("server");
|
|
1213
|
+
}
|
|
1214
|
+
return {
|
|
1215
|
+
epic_run_id: p["epic_run_id"],
|
|
1216
|
+
scope_id: p["scope_id"],
|
|
1217
|
+
before: parseRepairNullableString(p["before"]),
|
|
1218
|
+
after: parseRepairNullableString(p["after"]),
|
|
1219
|
+
committed: p["committed"],
|
|
1220
|
+
event_recorded: p["event_recorded"],
|
|
1221
|
+
outcome: typeof p["outcome"] === "string" ? p["outcome"] : "",
|
|
1222
|
+
parse_scheduled: p["parse_scheduled"],
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
function parseDispatchRemediationResult(parsed) {
|
|
1226
|
+
const p = parseRepairBase(parsed);
|
|
1227
|
+
if (typeof p["ticket_key"] !== "string") {
|
|
1228
|
+
throw new ConductorBridgeApiError("server");
|
|
1229
|
+
}
|
|
1230
|
+
const jobId = p["job_id"];
|
|
1231
|
+
return {
|
|
1232
|
+
epic_run_id: p["epic_run_id"],
|
|
1233
|
+
ticket_key: p["ticket_key"],
|
|
1234
|
+
before: parseRepairNullableString(p["before"]),
|
|
1235
|
+
after: parseRepairNullableString(p["after"]),
|
|
1236
|
+
committed: p["committed"],
|
|
1237
|
+
event_recorded: p["event_recorded"],
|
|
1238
|
+
job_type: typeof p["job_type"] === "string" ? p["job_type"] : "",
|
|
1239
|
+
job_id: typeof jobId === "number" && Number.isSafeInteger(jobId) ? jobId : null,
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
function parseMergeChildRouteResponse(parsed) {
|
|
1243
|
+
const p = parseRepairBase(parsed);
|
|
1244
|
+
const prNumber = p["pr_number"];
|
|
1245
|
+
if (typeof prNumber !== "number" || !Number.isSafeInteger(prNumber) || prNumber <= 0) {
|
|
1246
|
+
throw new ConductorBridgeApiError("server");
|
|
1247
|
+
}
|
|
1248
|
+
let localExecution = null;
|
|
1249
|
+
const local = p["local_execution"];
|
|
1250
|
+
if (local && typeof local === "object" && !Array.isArray(local)) {
|
|
1251
|
+
const l = local;
|
|
1252
|
+
const sha = normalizeSha(l["expected_head_sha"]);
|
|
1253
|
+
if (sha === null) {
|
|
1254
|
+
// An approval that names no usable head cannot be executed safely.
|
|
1255
|
+
throw new ConductorBridgeApiError("server");
|
|
1256
|
+
}
|
|
1257
|
+
localExecution = {
|
|
1258
|
+
expected_head_sha: sha,
|
|
1259
|
+
merge_method: parseRepairNullableString(l["merge_method"]),
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1262
|
+
return {
|
|
1263
|
+
epic_run_id: p["epic_run_id"],
|
|
1264
|
+
pr_number: prNumber,
|
|
1265
|
+
ticket_key: typeof p["ticket_key"] === "string" ? p["ticket_key"] : "",
|
|
1266
|
+
before: parseRepairNullableString(p["before"]),
|
|
1267
|
+
after: parseRepairNullableString(p["after"]),
|
|
1268
|
+
committed: p["committed"],
|
|
1269
|
+
event_recorded: p["event_recorded"],
|
|
1270
|
+
reason: parseRepairNullableString(p["reason"]),
|
|
1271
|
+
local_execution: localExecution,
|
|
1272
|
+
};
|
|
1273
|
+
}
|
|
1274
|
+
async function postRepairRequest(access, apiPath, body, fetchImpl) {
|
|
1275
|
+
return fetchConductorJsonWithMethodAndTimeout("POST", buildConductorJiraUrl(access.baseUrl, apiPath), conductorPostHeaders(access), JSON.stringify({ repo_name: access.repoName, ...body }), CONDUCTOR_FETCH_TIMEOUT_MS, fetchImpl,
|
|
1276
|
+
// The server's refusal explanation is the operator-facing message here.
|
|
1277
|
+
{ readErrorField: true });
|
|
1278
|
+
}
|
|
1279
|
+
/**
|
|
1280
|
+
* POST `/jira/epic-runs/runs/{epic_run_id}/scope-reparse` — re-drive the run's
|
|
1281
|
+
* OWN index scope through the retry-reparse lifecycle. Body: `{repo_name,
|
|
1282
|
+
* scope_id}`. The server refuses a scope not bound to the run (`404
|
|
1283
|
+
* TICKET_NOT_IN_RUN`) with nothing written.
|
|
1284
|
+
*/
|
|
1285
|
+
export async function scopeReparse(access, request, fetchImpl = globalThis.fetch) {
|
|
1286
|
+
requireNonEmptyString(request.epicRunId);
|
|
1287
|
+
requireNonEmptyString(request.scopeId);
|
|
1288
|
+
const parsed = await postRepairRequest(access, `${epicRunApiPath(request.epicRunId)}/scope-reparse`, { scope_id: request.scopeId }, fetchImpl);
|
|
1289
|
+
return parseScopeReparseResult(parsed);
|
|
1290
|
+
}
|
|
1291
|
+
/**
|
|
1292
|
+
* POST `/jira/epic-runs/runs/{epic_run_id}/tickets/{ticket_key}/dispatch-remediation`
|
|
1293
|
+
* — body `{repo_name, context}`. The SERVER selects `remediate` or
|
|
1294
|
+
* `reconcile_overlap` and admits the job through the reconciler's own sequence;
|
|
1295
|
+
* this client never names a job type.
|
|
1296
|
+
*/
|
|
1297
|
+
export async function dispatchRemediation(access, request, fetchImpl = globalThis.fetch) {
|
|
1298
|
+
requireNonEmptyString(request.epicRunId);
|
|
1299
|
+
requireNonEmptyString(request.ticketKey);
|
|
1300
|
+
requireNonEmptyString(request.context);
|
|
1301
|
+
const parsed = await postRepairRequest(access, `${epicRunApiPath(request.epicRunId)}/tickets/${encodeURIComponent(request.ticketKey)}/dispatch-remediation`, { context: request.context }, fetchImpl);
|
|
1302
|
+
return parseDispatchRemediationResult(parsed);
|
|
1303
|
+
}
|
|
1304
|
+
/**
|
|
1305
|
+
* POST `/jira/epic-runs/runs/{epic_run_id}/merge-child` — body EXACTLY
|
|
1306
|
+
* `{repo_name, pr_number}`. The server composes the head, gate, and action key.
|
|
1307
|
+
* When the repository merges locally the response carries a `local_execution`
|
|
1308
|
+
* approval the caller completes with {@link completeMergeChild}.
|
|
1309
|
+
*/
|
|
1310
|
+
export async function mergeChild(access, request, fetchImpl = globalThis.fetch) {
|
|
1311
|
+
requireNonEmptyString(request.epicRunId);
|
|
1312
|
+
requirePositiveSafeInteger(request.prNumber);
|
|
1313
|
+
const parsed = await postRepairRequest(access, `${epicRunApiPath(request.epicRunId)}/merge-child`, { pr_number: request.prNumber }, fetchImpl);
|
|
1314
|
+
return parseMergeChildRouteResponse(parsed);
|
|
1315
|
+
}
|
|
1316
|
+
/**
|
|
1317
|
+
* POST `/jira/epic-runs/runs/{epic_run_id}/merge-child/complete` — body
|
|
1318
|
+
* `{repo_name, pr_number, result, reason?}`. No SHA and no action key: the server
|
|
1319
|
+
* recomposes the merge identity and reads the merge commit from the provider.
|
|
1320
|
+
*/
|
|
1321
|
+
export async function completeMergeChild(access, request, fetchImpl = globalThis.fetch) {
|
|
1322
|
+
requireNonEmptyString(request.epicRunId);
|
|
1323
|
+
requirePositiveSafeInteger(request.prNumber);
|
|
1324
|
+
if (!["merged", "already_merged", "failed"].includes(request.result)) {
|
|
1325
|
+
throw new ConductorBridgeApiError("invalid-input");
|
|
1326
|
+
}
|
|
1327
|
+
const parsed = await postRepairRequest(access, `${epicRunApiPath(request.epicRunId)}/merge-child/complete`, {
|
|
1328
|
+
pr_number: request.prNumber,
|
|
1329
|
+
result: request.result,
|
|
1330
|
+
...(request.reason !== undefined ? { reason: request.reason } : {}),
|
|
1331
|
+
}, fetchImpl);
|
|
1332
|
+
return parseMergeChildRouteResponse(parsed);
|
|
1333
|
+
}
|
|
1163
1334
|
/**
|
|
1164
1335
|
* Idempotently seed an epic ticket status row via POST to the per-epic tickets
|
|
1165
1336
|
* endpoint. The backend uses ON CONFLICT DO NOTHING so repeated seeding across
|
|
@@ -1765,6 +1936,8 @@ export const CONDUCTOR_REVIEW_ALIGNMENT_STATUSES = new Set([
|
|
|
1765
1936
|
"not_configured",
|
|
1766
1937
|
"invalid",
|
|
1767
1938
|
]);
|
|
1939
|
+
/** Upper bound of the server's capped `fresh_reconciler_count` ("two or more"). */
|
|
1940
|
+
export const READINESS_FRESH_RECONCILER_COUNT_CAP = 2;
|
|
1768
1941
|
const READINESS_SOURCES = new Set(["epic", "project_default", "none"]);
|
|
1769
1942
|
const ACTIONS_LEVELS = new Set(["write", "read", "none", "unknown"]);
|
|
1770
1943
|
function readinessShapeError() {
|
|
@@ -1916,6 +2089,32 @@ export function parseConductorReadinessResponse(body) {
|
|
|
1916
2089
|
},
|
|
1917
2090
|
review_workflow: parseReviewWorkflow(root),
|
|
1918
2091
|
conductor_ci_workflow: parseConductorCiWorkflow(root),
|
|
2092
|
+
unattended: parseUnattended(root),
|
|
2093
|
+
bring_up: parseBringUp(root),
|
|
2094
|
+
};
|
|
2095
|
+
}
|
|
2096
|
+
/**
|
|
2097
|
+
* Parse the optional `bring_up` block (BAPI-1153). Same older-server rule as the
|
|
2098
|
+
* blocks above: absent (or null) is `null`; a PRESENT block is validated
|
|
2099
|
+
* strictly, every field required, and the capped count range-checked. A
|
|
2100
|
+
* malformed block throws the same field-free shape error as everything else.
|
|
2101
|
+
*/
|
|
2102
|
+
function parseBringUp(body) {
|
|
2103
|
+
const raw = body.bring_up;
|
|
2104
|
+
if (raw === undefined || raw === null)
|
|
2105
|
+
return null;
|
|
2106
|
+
const o = requireObject(raw);
|
|
2107
|
+
const freshCount = requireInt(o, "fresh_reconciler_count");
|
|
2108
|
+
if (freshCount < 0 || freshCount > READINESS_FRESH_RECONCILER_COUNT_CAP) {
|
|
2109
|
+
throw readinessShapeError();
|
|
2110
|
+
}
|
|
2111
|
+
return {
|
|
2112
|
+
migration_readable: requireBool(o, "migration_readable"),
|
|
2113
|
+
database_migration_current: requireBool(o, "database_migration_current"),
|
|
2114
|
+
heartbeat_readable: requireBool(o, "heartbeat_readable"),
|
|
2115
|
+
worker_present: requireBool(o, "worker_present"),
|
|
2116
|
+
fresh_reconciler_count: freshCount,
|
|
2117
|
+
single_transition_authority: requireBool(o, "single_transition_authority"),
|
|
1919
2118
|
};
|
|
1920
2119
|
}
|
|
1921
2120
|
/**
|
|
@@ -1949,6 +2148,28 @@ function parseConductorCiWorkflow(body) {
|
|
|
1949
2148
|
migration_guard_present: requireBool(o, "migration_guard_present"),
|
|
1950
2149
|
};
|
|
1951
2150
|
}
|
|
2151
|
+
/**
|
|
2152
|
+
* Parse the optional `unattended` block (BAPI-1102). Same rule as above.
|
|
2153
|
+
*
|
|
2154
|
+
* `notify_webhook_default_verified` is the one tri-state: absent is coerced to
|
|
2155
|
+
* `null` rather than to `false`, because "not verified here" and "verified as
|
|
2156
|
+
* broken" are different statements and only one of them is evidence. Every other
|
|
2157
|
+
* field is a strict boolean and a wrong type throws.
|
|
2158
|
+
*/
|
|
2159
|
+
function parseUnattended(body) {
|
|
2160
|
+
const raw = body.unattended;
|
|
2161
|
+
if (raw === undefined || raw === null)
|
|
2162
|
+
return null;
|
|
2163
|
+
const o = requireObject(raw);
|
|
2164
|
+
return {
|
|
2165
|
+
conductor_allowed: requireBool(o, "conductor_allowed"),
|
|
2166
|
+
notify_webhook_default_declared: requireBool(o, "notify_webhook_default_declared"),
|
|
2167
|
+
notify_webhook_default_verified: o.notify_webhook_default_verified === undefined
|
|
2168
|
+
? null
|
|
2169
|
+
: requireNullableBool(o, "notify_webhook_default_verified"),
|
|
2170
|
+
repository_readiness_confirmed: requireBool(o, "repository_readiness_confirmed"),
|
|
2171
|
+
};
|
|
2172
|
+
}
|
|
1952
2173
|
/**
|
|
1953
2174
|
* GET `/jira/epic-runs/conductor-readiness?repo_name=<repo>`.
|
|
1954
2175
|
*
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/** The pinned operator runbook every remedy below points into. */
|
|
2
|
+
export const BRING_UP_RUNBOOK = "docs/claude/epic-conductor-v2-operator-runbook.md";
|
|
3
|
+
/**
|
|
4
|
+
* The client-derived refusal vocabulary, IN REFUSAL ORDER.
|
|
5
|
+
*
|
|
6
|
+
* Kebab case, because these are computed by the client (the server reports
|
|
7
|
+
* facts, not refusals) and `second-reconciler-detected` is the mandated name;
|
|
8
|
+
* the snake-case unattended reasons mirror SERVER codes and are a separate
|
|
9
|
+
* vocabulary. Each fact's "unreadable" precedes its "bad", so an operator is
|
|
10
|
+
* never told to repair a condition that could not even be read.
|
|
11
|
+
*/
|
|
12
|
+
export const BRING_UP_REFUSAL_REASONS = [
|
|
13
|
+
"bring-up-facts-unreported",
|
|
14
|
+
"database-migration-unreadable",
|
|
15
|
+
"database-migration-behind",
|
|
16
|
+
"worker-heartbeat-unreadable",
|
|
17
|
+
"worker-not-present",
|
|
18
|
+
"second-reconciler-detected",
|
|
19
|
+
];
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Fixed remediations
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
export const BRING_UP_UNREPORTED_REMEDIATION = "upgrade the Bridge API deploy to a build that reports the bring-up safety facts; " +
|
|
24
|
+
"their state is unknown here, not healthy.";
|
|
25
|
+
export const DATABASE_MIGRATION_UNREADABLE_REMEDIATION = "retry; the migration state could not be read, which is not the same as confirmed-behind. " +
|
|
26
|
+
`If it persists, check the applied revision with \`alembic current\` (${BRING_UP_RUNBOOK} §3d).`;
|
|
27
|
+
export const DATABASE_MIGRATION_BEHIND_REMEDIATION = "confirm the applied revision with `alembic current` (not `alembic heads`, which reads the " +
|
|
28
|
+
"migration scripts), apply this checkout's migrations with `alembic -c alembic.ini upgrade head`, " +
|
|
29
|
+
`then retry (${BRING_UP_RUNBOOK} §3d).`;
|
|
30
|
+
export const WORKER_HEARTBEAT_UNREADABLE_REMEDIATION = "retry once the Bridge API database is reachable; the reconciler heartbeat could not be read, " +
|
|
31
|
+
"so worker presence is unknown, not confirmed absent.";
|
|
32
|
+
export const WORKER_NOT_PRESENT_REMEDIATION = "start a worker (`python worker.py`, or bring up a local plane) — a server started with " +
|
|
33
|
+
"DISABLE_SCHEDULER=true runs none. Create and approve only against a server that has a " +
|
|
34
|
+
`worker (${BRING_UP_RUNBOOK} §3d).`;
|
|
35
|
+
export const SECOND_RECONCILER_REMEDIATION = "find and stop the extra reconciler: check `ps aux | grep worker.py` for a reparented " +
|
|
36
|
+
`survivor of an earlier plane (${BRING_UP_RUNBOOK} §6), and note that a dev server started ` +
|
|
37
|
+
"without DISABLE_SCHEDULER=true runs a reconciler too. A reconciler stopped in the last 120 " +
|
|
38
|
+
"seconds still counts — heartbeat rows are not deleted on shutdown — so wait and retry.";
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Classification
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
const UNREPORTED_DETAIL = "not reported by this Bridge API deploy";
|
|
43
|
+
function unreported(id) {
|
|
44
|
+
return {
|
|
45
|
+
id,
|
|
46
|
+
state: "unreported",
|
|
47
|
+
detail: UNREPORTED_DETAIL,
|
|
48
|
+
remediation: BRING_UP_UNREPORTED_REMEDIATION,
|
|
49
|
+
refusal: "bring-up-facts-unreported",
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/** Database migration head: behind refuses; ahead is the intended model. */
|
|
53
|
+
export function classifyDatabaseMigration(bringUp) {
|
|
54
|
+
const id = "database-migration-current";
|
|
55
|
+
if (bringUp === null)
|
|
56
|
+
return unreported(id);
|
|
57
|
+
if (!bringUp.migration_readable) {
|
|
58
|
+
return {
|
|
59
|
+
id,
|
|
60
|
+
state: "unreadable",
|
|
61
|
+
detail: "the migration state could not be read (unknown, not confirmed behind)",
|
|
62
|
+
remediation: DATABASE_MIGRATION_UNREADABLE_REMEDIATION,
|
|
63
|
+
refusal: "database-migration-unreadable",
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
if (!bringUp.database_migration_current) {
|
|
67
|
+
return {
|
|
68
|
+
id,
|
|
69
|
+
state: "bad",
|
|
70
|
+
detail: "the database is BEHIND the server's code migration head",
|
|
71
|
+
remediation: DATABASE_MIGRATION_BEHIND_REMEDIATION,
|
|
72
|
+
refusal: "database-migration-behind",
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
return { id, state: "ok", detail: "the database is at (or ahead of) the code migration head" };
|
|
76
|
+
}
|
|
77
|
+
/** Worker presence, from the durable reconciler heartbeat. */
|
|
78
|
+
export function classifyWorkerPresent(bringUp) {
|
|
79
|
+
const id = "worker-present";
|
|
80
|
+
if (bringUp === null)
|
|
81
|
+
return unreported(id);
|
|
82
|
+
if (!bringUp.heartbeat_readable) {
|
|
83
|
+
return {
|
|
84
|
+
id,
|
|
85
|
+
state: "unreadable",
|
|
86
|
+
detail: "the reconciler heartbeat could not be read (unknown, not confirmed absent)",
|
|
87
|
+
remediation: WORKER_HEARTBEAT_UNREADABLE_REMEDIATION,
|
|
88
|
+
refusal: "worker-heartbeat-unreadable",
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
if (!bringUp.worker_present) {
|
|
92
|
+
return {
|
|
93
|
+
id,
|
|
94
|
+
state: "bad",
|
|
95
|
+
detail: "0 fresh reconcilers — no worker is running against this deployment",
|
|
96
|
+
remediation: WORKER_NOT_PRESENT_REMEDIATION,
|
|
97
|
+
refusal: "worker-not-present",
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return { id, state: "ok", detail: "a reconciler heartbeat is fresh" };
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Single transition authority. Refuses only on TWO OR MORE fresh reconcilers;
|
|
104
|
+
* zero is the worker fact's finding and is not repeated here as a refusal.
|
|
105
|
+
*/
|
|
106
|
+
export function classifySingleTransitionAuthority(bringUp) {
|
|
107
|
+
const id = "single-transition-authority";
|
|
108
|
+
if (bringUp === null)
|
|
109
|
+
return unreported(id);
|
|
110
|
+
if (!bringUp.heartbeat_readable) {
|
|
111
|
+
return {
|
|
112
|
+
id,
|
|
113
|
+
state: "unreadable",
|
|
114
|
+
detail: "the reconciler heartbeat could not be read (unknown, not confirmed)",
|
|
115
|
+
remediation: WORKER_HEARTBEAT_UNREADABLE_REMEDIATION,
|
|
116
|
+
refusal: "worker-heartbeat-unreadable",
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
if (bringUp.fresh_reconciler_count >= 2) {
|
|
120
|
+
return {
|
|
121
|
+
id,
|
|
122
|
+
state: "bad",
|
|
123
|
+
detail: "2+ fresh reconcilers — more than one transition authority is ticking",
|
|
124
|
+
remediation: SECOND_RECONCILER_REMEDIATION,
|
|
125
|
+
refusal: "second-reconciler-detected",
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
if (bringUp.fresh_reconciler_count === 0) {
|
|
129
|
+
return {
|
|
130
|
+
id,
|
|
131
|
+
state: "bad",
|
|
132
|
+
detail: "0 fresh reconcilers — no transition authority is ticking",
|
|
133
|
+
remediation: WORKER_NOT_PRESENT_REMEDIATION,
|
|
134
|
+
refusal: "worker-not-present",
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
return { id, state: "ok", detail: "exactly 1 fresh reconciler" };
|
|
138
|
+
}
|
|
139
|
+
/** All three verdicts, in render order. */
|
|
140
|
+
export function classifyBringUpFacts(bringUp) {
|
|
141
|
+
return [
|
|
142
|
+
classifyDatabaseMigration(bringUp),
|
|
143
|
+
classifyWorkerPresent(bringUp),
|
|
144
|
+
classifySingleTransitionAuthority(bringUp),
|
|
145
|
+
];
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* The FIRST refusal the block produces, in {@link BRING_UP_REFUSAL_REASONS}
|
|
149
|
+
* order, or `null` when all three facts hold.
|
|
150
|
+
*
|
|
151
|
+
* Order comes from the vocabulary constant, never from the order the verdicts
|
|
152
|
+
* happened to be computed, so a given combination of failures always names the
|
|
153
|
+
* same reason.
|
|
154
|
+
*/
|
|
155
|
+
export function firstBringUpRefusal(bringUp) {
|
|
156
|
+
const reasons = new Set(classifyBringUpFacts(bringUp)
|
|
157
|
+
.map((verdict) => verdict.refusal)
|
|
158
|
+
.filter((reason) => reason !== undefined));
|
|
159
|
+
return BRING_UP_REFUSAL_REASONS.find((reason) => reasons.has(reason)) ?? null;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* The operator sentence for one refusal: the fact, the observed condition, and
|
|
163
|
+
* one concrete remedy. The caller appends its own closing sentence ("No run was
|
|
164
|
+
* created."), because only the caller knows what it did not do.
|
|
165
|
+
*/
|
|
166
|
+
export const BRING_UP_REFUSAL_MESSAGES = {
|
|
167
|
+
"bring-up-facts-unreported": "the Bridge API did not report the bring-up safety facts (database migration, worker " +
|
|
168
|
+
"presence, single reconciler), so none of them could be confirmed. " +
|
|
169
|
+
capitalize(BRING_UP_UNREPORTED_REMEDIATION),
|
|
170
|
+
"database-migration-unreadable": "database migration: the server could not read whether its database is at the migration " +
|
|
171
|
+
"head, so this is unknown, not confirmed behind. " +
|
|
172
|
+
capitalize(DATABASE_MIGRATION_UNREADABLE_REMEDIATION),
|
|
173
|
+
"database-migration-behind": "database migration: the server's database is BEHIND its code's migration head, so the " +
|
|
174
|
+
"code would run against a schema that was never applied. " +
|
|
175
|
+
capitalize(DATABASE_MIGRATION_BEHIND_REMEDIATION),
|
|
176
|
+
"worker-heartbeat-unreadable": "worker presence: the durable reconciler heartbeat could not be read, so whether a worker " +
|
|
177
|
+
"is running is unknown, not confirmed absent. " +
|
|
178
|
+
capitalize(WORKER_HEARTBEAT_UNREADABLE_REMEDIATION),
|
|
179
|
+
"worker-not-present": "worker presence: no reconciler heartbeat is fresh, so nothing would drive this run. " +
|
|
180
|
+
capitalize(WORKER_NOT_PRESENT_REMEDIATION),
|
|
181
|
+
"second-reconciler-detected": "single transition authority: two or more reconcilers are ticking the shared database, " +
|
|
182
|
+
"and two transition authorities wedge an epic. " +
|
|
183
|
+
capitalize(SECOND_RECONCILER_REMEDIATION),
|
|
184
|
+
};
|
|
185
|
+
function capitalize(text) {
|
|
186
|
+
return text.length === 0 ? text : text[0].toUpperCase() + text.slice(1);
|
|
187
|
+
}
|
|
@@ -175,10 +175,12 @@ export function buildConductorEmitEventArgs(event) {
|
|
|
175
175
|
return args;
|
|
176
176
|
}
|
|
177
177
|
/**
|
|
178
|
-
* Resolve how to invoke `
|
|
179
|
-
* (run via the current Node executable so no global
|
|
180
|
-
* otherwise falls back to `BAPI_CONDUCTOR_BIN` or the
|
|
181
|
-
*
|
|
178
|
+
* Resolve how to invoke `epic-implementer emit-event`. Prefers
|
|
179
|
+
* `BAPI_CONDUCTOR_CLI_FILE` (run via the current Node executable so no global
|
|
180
|
+
* install is required), otherwise falls back to `BAPI_CONDUCTOR_BIN` or the
|
|
181
|
+
* bare `epic-implementer` binary on PATH — never the retained `conductor`
|
|
182
|
+
* name, which is now a fixed migration stub (S4/BAPI-1080). `emitArgs` are
|
|
183
|
+
* appended after the `emit-event` subcommand.
|
|
182
184
|
*/
|
|
183
185
|
export function resolveConductorEmitCommand(env, emitArgs) {
|
|
184
186
|
const cliFile = env.BAPI_CONDUCTOR_CLI_FILE;
|
|
@@ -188,7 +190,7 @@ export function resolveConductorEmitCommand(env, emitArgs) {
|
|
|
188
190
|
args: [cliFile.trim(), "emit-event", ...emitArgs],
|
|
189
191
|
};
|
|
190
192
|
}
|
|
191
|
-
const bin = nonEmpty(env.BAPI_CONDUCTOR_BIN) ? env.BAPI_CONDUCTOR_BIN.trim() : "
|
|
193
|
+
const bin = nonEmpty(env.BAPI_CONDUCTOR_BIN) ? env.BAPI_CONDUCTOR_BIN.trim() : "epic-implementer";
|
|
192
194
|
return { command: bin, args: ["emit-event", ...emitArgs] };
|
|
193
195
|
}
|
|
194
196
|
const defaultConductorSpawn = (command, args, input) => {
|
package/build/conductor/cli.js
CHANGED
|
@@ -18,6 +18,7 @@ import { ConductorValidationError, ConductorEpicTickV1FrozenError, toConductorEr
|
|
|
18
18
|
import { emitConductorEvent, purgeConductorLedger, sendWorkerMessage, checkWorkerMessages, } from "./store.js";
|
|
19
19
|
import { isDuplicateConstraintError } from "./producer-ledger.js";
|
|
20
20
|
import { SEMANTIC_EVENT_TYPES } from "./taxonomy.js";
|
|
21
|
+
import { DRIVER_CONTEXT_MAX_BYTES } from "./repair-contract.js";
|
|
21
22
|
import { installConductorGitHooks, resolveConductorHookBin } from "./git-hooks.js";
|
|
22
23
|
import { runFileScopeGuardCli } from "./file-scope-guard.js";
|
|
23
24
|
import { runPostCommitHookProducer, runReferenceTransactionHookProducer } from "./git-producer.js";
|
|
@@ -78,6 +79,15 @@ export function getConductorUsage() {
|
|
|
78
79
|
" adopt-current-head-and-unpark",
|
|
79
80
|
" Recover a ticket parked by a PR-head drift: adopt the",
|
|
80
81
|
" CURRENT PR head and unpark in one step.",
|
|
82
|
+
" scope-reparse --epic-run-id <id> --scope-id <scope>",
|
|
83
|
+
" Re-drive the run's OWN index scope through retry-reparse",
|
|
84
|
+
" and schedule its parse. Refused if the scope is not the run's.",
|
|
85
|
+
" dispatch-remediation --epic-run-id <id> --ticket-key <key> --context-file <path>",
|
|
86
|
+
` Dispatch a remediation job carrying driver context (UTF-8,`,
|
|
87
|
+
` at most DRIVER_CONTEXT_MAX_BYTES = ${DRIVER_CONTEXT_MAX_BYTES} bytes; never truncated).`,
|
|
88
|
+
" merge-child --epic-run-id <id> --pr <number>",
|
|
89
|
+
" Merge the run's PR-bound child through the shared merge",
|
|
90
|
+
" service; the server composes the head and gate identity.",
|
|
81
91
|
"",
|
|
82
92
|
"supervise options:",
|
|
83
93
|
" --run-id <id> Run/session identifier to supervise (required)",
|
|
@@ -241,6 +251,10 @@ const VALID_COMMANDS = new Set([
|
|
|
241
251
|
"abandon-run",
|
|
242
252
|
"unpark",
|
|
243
253
|
"adopt-current-head-and-unpark",
|
|
254
|
+
// BAPI-1154: CLI-only run-addressed repair verbs — never registered as MCP tools.
|
|
255
|
+
"scope-reparse",
|
|
256
|
+
"dispatch-remediation",
|
|
257
|
+
"merge-child",
|
|
244
258
|
// Private, and deliberately ABSENT from the usage text: `__hook-bin` exists as
|
|
245
259
|
// the bundled-artifact regression guard for BAPI-772 (mirroring `plane
|
|
246
260
|
// __entrypoint`), not as a supported operator workflow.
|
|
@@ -1237,6 +1251,20 @@ export async function runConductorCli(argv) {
|
|
|
1237
1251
|
const { runAdoptCurrentHeadAndUnparkCommand } = await import("./recovery-cli.js");
|
|
1238
1252
|
return await runAdoptCurrentHeadAndUnparkCommand(parsed.argv);
|
|
1239
1253
|
}
|
|
1254
|
+
// BAPI-1154: the three run-addressed repair verbs — CLI-only, and lazily
|
|
1255
|
+
// imported exactly like the four recovery verbs above.
|
|
1256
|
+
case "scope-reparse": {
|
|
1257
|
+
const { runScopeReparseCommand } = await import("./recovery-cli.js");
|
|
1258
|
+
return await runScopeReparseCommand(parsed.argv);
|
|
1259
|
+
}
|
|
1260
|
+
case "dispatch-remediation": {
|
|
1261
|
+
const { runDispatchRemediationCommand } = await import("./recovery-cli.js");
|
|
1262
|
+
return await runDispatchRemediationCommand(parsed.argv);
|
|
1263
|
+
}
|
|
1264
|
+
case "merge-child": {
|
|
1265
|
+
const { runMergeChildCommand } = await import("./recovery-cli.js");
|
|
1266
|
+
return await runMergeChildCommand(parsed.argv);
|
|
1267
|
+
}
|
|
1240
1268
|
default:
|
|
1241
1269
|
console.error('Error: Unknown command. Run "conductor --help" for usage.');
|
|
1242
1270
|
return 1;
|