@bridge_gpt/mcp-server 0.2.66 → 0.2.67
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 +252 -77
- package/build/agent-capabilities/probes.js +15 -5
- package/build/agent-launchers/claude-executor-adapter.js +43 -16
- package/build/agent-launchers/executor-adapter.js +7 -4
- package/build/agents.generated.js +1 -1
- package/build/bounded-wait.js +67 -10
- package/build/commands.generated.js +11 -11
- package/build/conductor/bridge-api-client.js +143 -1
- package/build/conductor/bring-up-facts.js +1 -1
- package/build/conductor/doctor.js +3 -3
- package/build/conductor/epic-implementer-cli.js +22 -6
- package/build/conductor/errors.js +4 -3
- package/build/conductor/guided-repair.js +6 -2
- package/build/conductor/hook-runtime-qualification.js +1 -1
- package/build/conductor/merge-execution-contract.js +27 -8
- package/build/conductor/merge-execution-eligibility.js +3 -2
- package/build/conductor/pr-ci-producer.js +1 -1
- package/build/conductor/readiness-cli.js +18 -2
- package/build/conductor/readiness-sections.js +41 -6
- package/build/conductor/readiness.js +181 -29
- package/build/conductor/recovery-cli.js +200 -8
- package/build/conductor/recovery-operations.js +69 -3
- package/build/conductor/repair-contract.js +54 -3
- package/build/conductor/repository-workflow-remedy.js +125 -0
- package/build/conductor/tools.js +2 -2
- package/build/conductor-bundle-artifacts.js +8 -0
- package/build/docs.generated.js +8 -3
- package/build/drive-epic-key.js +6 -5
- package/build/drive-epic.js +85 -54
- package/build/epic-coherence-input.js +2 -1
- package/build/epic-implementer-bin.js +22 -20
- package/build/epic-implementer-bundle-cli.js +2 -2
- package/build/executor/cli.js +3 -2
- package/build/executor/completion-contract.js +48 -5
- package/build/executor/completion-supervision.js +4 -0
- package/build/executor/env.js +19 -0
- package/build/executor/heartbeat.js +1 -1
- package/build/executor/http-client.js +20 -0
- package/build/executor/job-errors.js +25 -0
- package/build/executor/job-runner.js +225 -41
- package/build/executor/observation.js +6 -1
- package/build/executor/preflight.js +51 -14
- package/build/executor/required-work-ledger.js +6 -1
- package/build/executor/resume-pre-spawn.js +2 -0
- package/build/executor/runner.js +8 -2
- package/build/executor/service-unit.js +7 -1
- package/build/executor/worker-finalization.js +82 -29
- package/build/executor/worker-guard-hook.js +137 -33
- package/build/executor/worktree.js +82 -1
- package/build/index.js +403 -429
- package/build/install-bridge-conductor.js +8 -6
- package/build/install-bridge.js +3 -3
- package/build/phase-context-git-probe.js +68 -0
- package/build/phase-result-artifacts.js +173 -5
- package/build/pipelines.generated.js +18 -24
- package/build/plane/alembic-head.js +12 -3
- package/build/plane/base-worktree-guard.js +147 -0
- package/build/plane/cli.js +217 -17
- package/build/plane/code-snapshot.js +468 -0
- package/build/plane/defaults.js +48 -3
- package/build/plane/endpoint.js +41 -0
- package/build/plane/manifest.js +120 -1
- package/build/plane/member-roster.js +94 -19
- package/build/plane/preflight.js +30 -2
- package/build/plane/process-cwd.js +67 -0
- package/build/plane/restart.js +542 -0
- package/build/plane/shutdown.js +31 -0
- package/build/plane/status.js +143 -8
- package/build/plane/supervisor.js +586 -179
- package/build/plane/types.js +54 -2
- package/build/printed-remedy-commands.js +37 -0
- package/build/readiness-check.js +1 -1
- package/build/readme.generated.js +1 -1
- package/build/review-generation.js +22 -0
- package/build/setup-epic.js +44 -19
- package/build/sfcc/config.js +9 -0
- package/build/sfcc/credentials.js +100 -38
- package/build/sfcc/hostname.js +67 -0
- package/build/sfcc/permissions.js +17 -13
- package/build/sfcc/register.js +1 -1
- package/build/sfcc/tool-wrapper.js +39 -43
- package/build/sfcc/write-grants.js +17 -11
- package/build/sfcc/write-result.js +4 -3
- package/build/sfcc/writes-site-preference.js +20 -8
- package/build/start-tickets.js +179 -31
- package/build/tool-error-envelope.js +61 -0
- package/build/version.generated.js +3 -3
- package/build/visual-diff.js +22 -1
- package/build/worker-guard-hook-bin.js +3 -6
- package/build/worker-guard-hook-main.js +47 -0
- package/build/worktree-core.js +130 -4
- package/docs/EPIC-IMPLEMENTER-INSTALL.md +849 -0
- package/docs/EPIC-IMPLEMENTER.md +662 -0
- package/docs/install/mcp-tool-integrations.md +24 -15
- package/docs/install/sfcc-integration.md +12 -4
- package/package.json +1 -1
- package/pipelines/plan-epic.json +0 -5
- package/pipelines/review-ticket.json +1 -1
- package/docs/CONDUCTOR.md +0 -605
- package/pipelines/idea-to-pr.json +0 -49
|
@@ -20,7 +20,7 @@ import { parseContinuationStatusResult, parseContinueParkedChildResult, } from "
|
|
|
20
20
|
import { resolveDeclaredRunBaseBranch } from "./run-branch.js";
|
|
21
21
|
import { safeEpicLaunchTargetUrl } from "../epic-launch-diagnostic.js";
|
|
22
22
|
import { validateHttpBaseUrl } from "../base-url.js";
|
|
23
|
-
import { isCiResumeRefusalCode } from "./repair-contract.js";
|
|
23
|
+
import { RERUN_CI_CHECK_STATUSES, RERUN_CI_NOT_RUNNABLE_REASONS, RERUN_CI_OUTCOMES, isCiResumeRefusalCode, } from "./repair-contract.js";
|
|
24
24
|
/** Default Bridge API base URL when `BAPI_BASE_URL` is unset. */
|
|
25
25
|
export const CONDUCTOR_DEFAULT_BASE_URL = "https://bridgegpt-api.com";
|
|
26
26
|
/** Default per-request timeout for conductor Bridge API calls. */
|
|
@@ -1532,6 +1532,117 @@ export async function dispatchRemediation(access, request, fetchImpl = globalThi
|
|
|
1532
1532
|
const parsed = await postRepairRequest(access, `${epicRunApiPath(request.epicRunId)}/tickets/${encodeURIComponent(request.ticketKey)}/dispatch-remediation`, withGuard({ context: request.context }, request.guard), fetchImpl);
|
|
1533
1533
|
return parseDispatchRemediationResult(parsed);
|
|
1534
1534
|
}
|
|
1535
|
+
// ---------------------------------------------------------------------------
|
|
1536
|
+
// BAPI-1388 — `rerun-ci`
|
|
1537
|
+
// ---------------------------------------------------------------------------
|
|
1538
|
+
/** The synthetic code an old server's missing route is reported under. */
|
|
1539
|
+
export const RERUN_CI_ROUTE_UNAVAILABLE_CODE = "RERUN_CI_ROUTE_UNAVAILABLE";
|
|
1540
|
+
/** The fixed operator text for a server that has no `rerun-ci` route. */
|
|
1541
|
+
export const RERUN_CI_ROUTE_UNAVAILABLE_MESSAGE = "rerun-ci is not available on this server; upgrade the Bridge API server before using it";
|
|
1542
|
+
const RERUN_CI_HEAD_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
|
|
1543
|
+
function parseRerunCiCheck(value) {
|
|
1544
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1545
|
+
throw new ConductorBridgeApiError("server");
|
|
1546
|
+
}
|
|
1547
|
+
const c = value;
|
|
1548
|
+
const status = c["status"];
|
|
1549
|
+
if (typeof status !== "string" ||
|
|
1550
|
+
!RERUN_CI_CHECK_STATUSES.includes(status)) {
|
|
1551
|
+
throw new ConductorBridgeApiError("server");
|
|
1552
|
+
}
|
|
1553
|
+
const reason = c["reason"];
|
|
1554
|
+
if (reason !== undefined &&
|
|
1555
|
+
reason !== null &&
|
|
1556
|
+
(typeof reason !== "string" ||
|
|
1557
|
+
!RERUN_CI_NOT_RUNNABLE_REASONS.includes(reason))) {
|
|
1558
|
+
throw new ConductorBridgeApiError("server");
|
|
1559
|
+
}
|
|
1560
|
+
const optionalPositive = (field) => {
|
|
1561
|
+
const v = c[field];
|
|
1562
|
+
if (v === undefined || v === null)
|
|
1563
|
+
return null;
|
|
1564
|
+
if (typeof v !== "number" || !Number.isSafeInteger(v) || v <= 0) {
|
|
1565
|
+
throw new ConductorBridgeApiError("server");
|
|
1566
|
+
}
|
|
1567
|
+
return v;
|
|
1568
|
+
};
|
|
1569
|
+
const optionalText = (field) => {
|
|
1570
|
+
const v = c[field];
|
|
1571
|
+
if (v === undefined || v === null)
|
|
1572
|
+
return null;
|
|
1573
|
+
if (typeof v !== "string")
|
|
1574
|
+
throw new ConductorBridgeApiError("server");
|
|
1575
|
+
return v;
|
|
1576
|
+
};
|
|
1577
|
+
return {
|
|
1578
|
+
name: optionalText("name"),
|
|
1579
|
+
workflow_identity: optionalText("workflow_identity"),
|
|
1580
|
+
run_id: optionalPositive("run_id"),
|
|
1581
|
+
status: status,
|
|
1582
|
+
reason: (reason ?? null),
|
|
1583
|
+
job_id: optionalPositive("job_id"),
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1586
|
+
/**
|
|
1587
|
+
* Strictly parse a `rerun-ci` success body. Anything short of the contract — an
|
|
1588
|
+
* unknown outcome or per-check status, a malformed head, a non-list `checks` —
|
|
1589
|
+
* throws, so a malformed answer is never read as a success.
|
|
1590
|
+
*/
|
|
1591
|
+
export function parseRerunCiResult(parsed) {
|
|
1592
|
+
const p = parseRepairBase(parsed);
|
|
1593
|
+
const outcome = p["outcome"];
|
|
1594
|
+
const checks = p["checks"];
|
|
1595
|
+
const head = p["head_sha"];
|
|
1596
|
+
if (typeof p["ticket_key"] !== "string" ||
|
|
1597
|
+
typeof outcome !== "string" ||
|
|
1598
|
+
!RERUN_CI_OUTCOMES.includes(outcome) ||
|
|
1599
|
+
!Array.isArray(checks) ||
|
|
1600
|
+
(head !== undefined && head !== null && (typeof head !== "string" || !RERUN_CI_HEAD_RE.test(head)))) {
|
|
1601
|
+
throw new ConductorBridgeApiError("server");
|
|
1602
|
+
}
|
|
1603
|
+
return {
|
|
1604
|
+
epic_run_id: p["epic_run_id"],
|
|
1605
|
+
ticket_key: p["ticket_key"],
|
|
1606
|
+
before: parseRepairNullableString(p["before"]),
|
|
1607
|
+
after: parseRepairNullableString(p["after"]),
|
|
1608
|
+
committed: p["committed"],
|
|
1609
|
+
event_recorded: p["event_recorded"],
|
|
1610
|
+
outcome: outcome,
|
|
1611
|
+
head_sha: typeof head === "string" ? head : null,
|
|
1612
|
+
checks: checks.map(parseRerunCiCheck),
|
|
1613
|
+
};
|
|
1614
|
+
}
|
|
1615
|
+
/**
|
|
1616
|
+
* POST `/jira/epic-runs/runs/{epic_run_id}/tickets/{ticket_key}/rerun-ci` — body
|
|
1617
|
+
* `{repo_name, guard}` and nothing else: no context, check list, or run id, because
|
|
1618
|
+
* the SERVER decides what is re-runnable from its own fresh, head-bound CI read.
|
|
1619
|
+
*
|
|
1620
|
+
* A generic `{"detail":"Not Found"}` 404 or a 405 means the server predates the
|
|
1621
|
+
* route; it is reported under {@link RERUN_CI_ROUTE_UNAVAILABLE_CODE} so no caller
|
|
1622
|
+
* can read it as a refusal of THIS ticket or as a success (following
|
|
1623
|
+
* {@link classifySupervisorBootstrapFailure}). A structured 404 — notably
|
|
1624
|
+
* `TICKET_NOT_IN_RUN` — keeps its own code and stays a refusal.
|
|
1625
|
+
*/
|
|
1626
|
+
export async function rerunCi(access, request, fetchImpl = globalThis.fetch) {
|
|
1627
|
+
requireNonEmptyString(request.epicRunId);
|
|
1628
|
+
requireNonEmptyString(request.ticketKey);
|
|
1629
|
+
let parsed;
|
|
1630
|
+
try {
|
|
1631
|
+
parsed = await postRepairRequest(access, `${epicRunApiPath(request.epicRunId)}/tickets/${encodeURIComponent(request.ticketKey)}/rerun-ci`, { guard: request.guard }, fetchImpl);
|
|
1632
|
+
}
|
|
1633
|
+
catch (err) {
|
|
1634
|
+
if (err instanceof ConductorBridgeApiError &&
|
|
1635
|
+
!err.errorCode &&
|
|
1636
|
+
(err.status === 404 || err.status === 405)) {
|
|
1637
|
+
throw new ConductorBridgeApiError("http", err.status, {
|
|
1638
|
+
errorCode: RERUN_CI_ROUTE_UNAVAILABLE_CODE,
|
|
1639
|
+
bodyPreview: RERUN_CI_ROUTE_UNAVAILABLE_MESSAGE,
|
|
1640
|
+
});
|
|
1641
|
+
}
|
|
1642
|
+
throw err;
|
|
1643
|
+
}
|
|
1644
|
+
return parseRerunCiResult(parsed);
|
|
1645
|
+
}
|
|
1535
1646
|
/**
|
|
1536
1647
|
* POST `/jira/epic-runs/runs/{epic_run_id}/merge-child` — body EXACTLY
|
|
1537
1648
|
* `{repo_name, pr_number}`. The server composes the head, gate, and action key.
|
|
@@ -2469,6 +2580,20 @@ function requireBool(o, key) {
|
|
|
2469
2580
|
throw readinessShapeError();
|
|
2470
2581
|
return v;
|
|
2471
2582
|
}
|
|
2583
|
+
/**
|
|
2584
|
+
* An optional boolean field as a spreadable fragment: `{}` when the key is absent,
|
|
2585
|
+
* `{ [key]: value }` when it is a boolean, and a shape error for anything else —
|
|
2586
|
+
* including an explicit `null`. Spreading keeps an absent field ABSENT rather than
|
|
2587
|
+
* present-as-`undefined`.
|
|
2588
|
+
*/
|
|
2589
|
+
function optionalBoolField(o, key) {
|
|
2590
|
+
if (!(key in o) || o[key] === undefined)
|
|
2591
|
+
return {};
|
|
2592
|
+
const v = o[key];
|
|
2593
|
+
if (typeof v !== "boolean")
|
|
2594
|
+
throw readinessShapeError();
|
|
2595
|
+
return { [key]: v };
|
|
2596
|
+
}
|
|
2472
2597
|
function requireInt(o, key) {
|
|
2473
2598
|
const v = o[key];
|
|
2474
2599
|
if (typeof v !== "number" || !Number.isInteger(v))
|
|
@@ -2617,6 +2742,15 @@ function parseRequiredChecksVerification(sup, requiredChecksCount) {
|
|
|
2617
2742
|
required_checks_evidence_fresh: fresh,
|
|
2618
2743
|
};
|
|
2619
2744
|
}
|
|
2745
|
+
/** Optional boolean: absent/null from an older server, a non-boolean fails closed. */
|
|
2746
|
+
function parseOptionalBool(o, key) {
|
|
2747
|
+
const v = o[key];
|
|
2748
|
+
if (v === undefined || v === null)
|
|
2749
|
+
return null;
|
|
2750
|
+
if (typeof v !== "boolean")
|
|
2751
|
+
throw readinessShapeError();
|
|
2752
|
+
return v;
|
|
2753
|
+
}
|
|
2620
2754
|
/** Optional positive-integer threshold: absent/null from an older server. */
|
|
2621
2755
|
function parseOptionalPositiveInt(o, key) {
|
|
2622
2756
|
const v = o[key];
|
|
@@ -2666,6 +2800,9 @@ export function parseConductorReadinessResponse(body) {
|
|
|
2666
2800
|
// Absent on a pre-BAPI-694 server, where no repository default can exist.
|
|
2667
2801
|
review_policy_present: sup.review_policy_present === true,
|
|
2668
2802
|
...parseRequiredChecksVerification(sup, requiredChecksCount),
|
|
2803
|
+
required_checks_retired_gate_configured: parseOptionalBool(sup, "required_checks_retired_gate_configured"),
|
|
2804
|
+
required_checks_ci_workflow_absent: parseOptionalBool(sup, "required_checks_ci_workflow_absent"),
|
|
2805
|
+
required_checks_review_workflow_absent: parseOptionalBool(sup, "required_checks_review_workflow_absent"),
|
|
2669
2806
|
},
|
|
2670
2807
|
github: {
|
|
2671
2808
|
credentials_readable: requireBool(gh, "credentials_readable"),
|
|
@@ -2891,6 +3028,11 @@ function parseConductorCiWorkflow(body) {
|
|
|
2891
3028
|
probe_succeeded: requireBool(o, "probe_succeeded"),
|
|
2892
3029
|
workflow_present: requireBool(o, "workflow_present"),
|
|
2893
3030
|
migration_guard_present: requireBool(o, "migration_guard_present"),
|
|
3031
|
+
// BAPI-1359: OPTIONAL so a newer CLI still reads an older server. An absent
|
|
3032
|
+
// key stays `undefined` — never materialized as a default — and a present key
|
|
3033
|
+
// must be a real boolean (`null`, strings, numbers, and objects all reject).
|
|
3034
|
+
...optionalBoolField(o, "migration_guard_required"),
|
|
3035
|
+
...optionalBoolField(o, "migration_chain_probe_succeeded"),
|
|
2894
3036
|
};
|
|
2895
3037
|
}
|
|
2896
3038
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** The pinned operator runbook every remedy below points into. */
|
|
2
|
-
export const BRING_UP_RUNBOOK = "docs/
|
|
2
|
+
export const BRING_UP_RUNBOOK = "docs/epic-implementer/operator-runbook.md";
|
|
3
3
|
/**
|
|
4
4
|
* The client-derived refusal vocabulary, IN REFUSAL ORDER.
|
|
5
5
|
*
|
|
@@ -822,9 +822,9 @@ export function formatConductorDoctorReport(report) {
|
|
|
822
822
|
lines.push(` - ${w}`);
|
|
823
823
|
}
|
|
824
824
|
lines.push("");
|
|
825
|
-
lines.push("Epic
|
|
826
|
-
lines.push("locally. To execute claimed jobs on this machine, run:");
|
|
827
|
-
lines.push(` npx -y ${MCP_PACKAGE_NAME} executor --repo <name>`);
|
|
825
|
+
lines.push("The Epic Implementer reconciles epics server-side — nothing to schedule");
|
|
826
|
+
lines.push("locally. To execute a run's claimed jobs on this machine, run:");
|
|
827
|
+
lines.push(` npx -y ${MCP_PACKAGE_NAME} executor --repo <name> --epic-run-id <run-id>`);
|
|
828
828
|
lines.push("");
|
|
829
829
|
lines.push("MCP Profile (optional, local)");
|
|
830
830
|
lines.push("─────────────────────────────");
|
|
@@ -57,18 +57,21 @@ export function getConductorUsage() {
|
|
|
57
57
|
return [
|
|
58
58
|
"Usage: epic-implementer <command> [options]",
|
|
59
59
|
"",
|
|
60
|
-
"Local append-only event ledger for multi-agent coordination
|
|
61
|
-
"
|
|
60
|
+
"Local append-only event ledger for multi-agent coordination, plus run-control",
|
|
61
|
+
"commands. The ledger commands talk ONLY to the local SQLite store",
|
|
62
|
+
"(~/.config/bridge/events.db); the run-control commands (stop-run through",
|
|
63
|
+
"rerun-ci), epic-status, and readiness call the Bridge API.",
|
|
62
64
|
"",
|
|
63
65
|
"Commands:",
|
|
64
66
|
" emit-event Append one semantic event to the ledger",
|
|
65
67
|
" supervise --run-id <id> Run the foreground, run-scoped supervisor loop",
|
|
66
|
-
" epic-tick
|
|
68
|
+
" epic-tick RETIRED (v1): always exits 1 with EPIC_TICK_V1_FROZEN; the",
|
|
69
|
+
" server-side reconciler replaced it",
|
|
67
70
|
" epic-status Print read-only health summary of an Epic Run",
|
|
68
71
|
" send-message Enqueue ONE typed supervisor->worker relay message (idempotent)",
|
|
69
72
|
" check-messages Read + ACK pending relay messages for a worker (no redelivery)",
|
|
70
73
|
" doctor Read-only health/diagnostics report (ledger + git hooks)",
|
|
71
|
-
" readiness Read-only ADVISORY report of every
|
|
74
|
+
" readiness Read-only ADVISORY report of every Epic Implementer prerequisite",
|
|
72
75
|
" (install + epic-implementer doctor + plane preflight + server),",
|
|
73
76
|
" each with pass/warn/fail/skip and one named remediation.",
|
|
74
77
|
" Remediations are NOT run automatically; always exits 0.",
|
|
@@ -115,6 +118,10 @@ export function getConductorUsage() {
|
|
|
115
118
|
" resume-ci --epic-run-id <id> --ticket-key <key> [--json]",
|
|
116
119
|
" Resume a child parked on a qualified CI stop once CI is green: CI",
|
|
117
120
|
" returns to evaluation with NO new wait, job, or repair allowance.",
|
|
121
|
+
" rerun-ci --epic-run-id <id> --ticket-key <key> --binding <token> [--json]",
|
|
122
|
+
" Ask CI to re-run a ticket's FAILED checks at its bound head (guided: copy",
|
|
123
|
+
" the command explain-run rendered). Charges NO allowance and never re-runs",
|
|
124
|
+
" claude-review. A re-run is not a verdict: after CI is green, use resume-ci.",
|
|
118
125
|
"",
|
|
119
126
|
"supervise options:",
|
|
120
127
|
" --run-id <id> Run/session identifier to supervise (required)",
|
|
@@ -196,11 +203,11 @@ export function getConductorUsage() {
|
|
|
196
203
|
" epic-implementer scope-catch-up --epic-run-id <id> --scope-id <scope>",
|
|
197
204
|
" # a merge landed on epic/<KEY> that the scope never observed (unobserved_advance)",
|
|
198
205
|
" epic-implementer patch-policy --epic-run-id <id> --patch-file required-checks.json",
|
|
199
|
-
" # required_checks_empty_no_mode; the file holds {\"required_checks\":[\"
|
|
206
|
+
" # required_checks_empty_no_mode; the file holds {\"required_checks\":[\"epic-implementer-ci / gate\"]}",
|
|
200
207
|
" epic-implementer resume-run --epic-run-id <id>",
|
|
201
208
|
" # a cap_wall_clock pause whose cause is now cleared",
|
|
202
209
|
"",
|
|
203
|
-
"epic-tick options:",
|
|
210
|
+
"epic-tick options (retired; parsed by no live path):",
|
|
204
211
|
" --epic-key <KEY> Epic key to supervise (required, non-empty)",
|
|
205
212
|
" --scheduled-at <epoch> Epoch-seconds timestamp when this tick was scheduled (optional)",
|
|
206
213
|
" --lease-ttl-seconds <n> Lease TTL in seconds (optional, default 120)",
|
|
@@ -259,6 +266,8 @@ export function getConductorUsage() {
|
|
|
259
266
|
" epic-implementer continuation-receipt --epic-run-id 3f9c2b7e-... --ticket-key BAPI-852",
|
|
260
267
|
" # CI finished green after the child parked on an expired CI wait:",
|
|
261
268
|
" epic-implementer resume-ci --epic-run-id 3f9c2b7e-... --ticket-key BAPI-852",
|
|
269
|
+
" # CI failed on infrastructure: re-run it (never claude-review); then resume-ci once green:",
|
|
270
|
+
" epic-implementer rerun-ci --epic-run-id 3f9c2b7e-... --ticket-key BAPI-852 --binding <token>",
|
|
262
271
|
].join("\n");
|
|
263
272
|
}
|
|
264
273
|
/**
|
|
@@ -312,6 +321,8 @@ const VALID_COMMANDS = new Set([
|
|
|
312
321
|
"continuation-receipt",
|
|
313
322
|
// BAPI-1326: CLI-only allowance-preserving CI resume — never an MCP tool.
|
|
314
323
|
"resume-ci",
|
|
324
|
+
// BAPI-1388: CLI-only guided provider re-run of failed CI workflow runs — never an MCP tool.
|
|
325
|
+
"rerun-ci",
|
|
315
326
|
// Private, and deliberately ABSENT from the usage text: `__hook-bin` exists as
|
|
316
327
|
// the bundled-artifact regression guard for BAPI-772 (mirroring `plane
|
|
317
328
|
// __entrypoint`), not as a supported operator workflow.
|
|
@@ -1451,6 +1462,11 @@ export async function runEpicImplementerCli(argv) {
|
|
|
1451
1462
|
const { runResumeCiCommand } = await import("./recovery-cli.js");
|
|
1452
1463
|
return await runResumeCiCommand(parsed.argv);
|
|
1453
1464
|
}
|
|
1465
|
+
case "rerun-ci": {
|
|
1466
|
+
// BAPI-1388: delegates only to the guided, pinned rerun-ci operation.
|
|
1467
|
+
const { runRerunCiCommand } = await import("./recovery-cli.js");
|
|
1468
|
+
return await runRerunCiCommand(parsed.argv);
|
|
1469
|
+
}
|
|
1454
1470
|
default:
|
|
1455
1471
|
console.error('Error: Unknown command. Run "epic-implementer --help" for usage.');
|
|
1456
1472
|
return 1;
|
|
@@ -70,9 +70,10 @@ export class ConductorStoreError extends Error {
|
|
|
70
70
|
*/
|
|
71
71
|
export class ConductorEpicTickV1FrozenError extends Error {
|
|
72
72
|
constructor() {
|
|
73
|
-
super("The v1 `epic-implementer epic-tick` path is frozen (EPIC_TICK_V1_FROZEN).
|
|
74
|
-
"
|
|
75
|
-
"plus the local executor — run `mcp-server executor
|
|
73
|
+
super("The v1 `epic-implementer epic-tick` path is frozen (EPIC_TICK_V1_FROZEN). The " +
|
|
74
|
+
"Epic Implementer replaced the local epic tick with a server-side reconciler " +
|
|
75
|
+
"plus the local executor — run `mcp-server executor --repo <name> " +
|
|
76
|
+
"--epic-run-id <run-id>` instead.");
|
|
76
77
|
this.name = "ConductorEpicTickV1FrozenError";
|
|
77
78
|
}
|
|
78
79
|
}
|
|
@@ -32,6 +32,9 @@ export const GUIDED_REPAIR_VERBS = [
|
|
|
32
32
|
"scope-reparse",
|
|
33
33
|
"scope-catch-up",
|
|
34
34
|
"dispatch-remediation",
|
|
35
|
+
// BAPI-1388 — ticket-addressed, pinned to the same version-and-head subject as
|
|
36
|
+
// `dispatch-remediation`, and consuming the binding ExplainRun renders.
|
|
37
|
+
"rerun-ci",
|
|
35
38
|
];
|
|
36
39
|
/** A malformed or mismatched binding. The message never echoes the token. */
|
|
37
40
|
export class GuidedBindingError extends Error {
|
|
@@ -57,6 +60,7 @@ const ALLOWED_KEYS = {
|
|
|
57
60
|
"scope-reparse": ["scope_id", "lifecycle_state", "required_commit_sha"],
|
|
58
61
|
"scope-catch-up": ["scope_id", "lifecycle_state", "required_commit_sha"],
|
|
59
62
|
"dispatch-remediation": ["ticket_key", "expected_row_version", "expected_head_sha"],
|
|
63
|
+
"rerun-ci": ["ticket_key", "expected_row_version", "expected_head_sha"],
|
|
60
64
|
};
|
|
61
65
|
function invalid() {
|
|
62
66
|
return new GuidedBindingError("--binding is not a valid guided-repair binding; nothing was sent. Copy the " +
|
|
@@ -148,7 +152,7 @@ export function decodeGuidedBinding(token) {
|
|
|
148
152
|
throw invalid();
|
|
149
153
|
}
|
|
150
154
|
}
|
|
151
|
-
else if (guidedVerb === "dispatch-remediation") {
|
|
155
|
+
else if (guidedVerb === "dispatch-remediation" || guidedVerb === "rerun-ci") {
|
|
152
156
|
binding.ticketKey = boundedString(raw.ticket_key, 64);
|
|
153
157
|
binding.expectedRowVersion = nonNegativeInt(raw.expected_row_version);
|
|
154
158
|
binding.expectedHeadSha = optionalSha(raw.expected_head_sha);
|
|
@@ -202,7 +206,7 @@ export function guardForBinding(binding, requestId) {
|
|
|
202
206
|
guard.block_reason = binding.blockReason;
|
|
203
207
|
return guard;
|
|
204
208
|
}
|
|
205
|
-
if (binding.verb === "dispatch-remediation") {
|
|
209
|
+
if (binding.verb === "dispatch-remediation" || binding.verb === "rerun-ci") {
|
|
206
210
|
return {
|
|
207
211
|
request_id: requestId,
|
|
208
212
|
epic_run_id: binding.epicRunId,
|
|
@@ -89,7 +89,7 @@ export const HOOK_RUNTIME_ACTIONS = {
|
|
|
89
89
|
"from a persistent install.",
|
|
90
90
|
"unqualified-runtime": "the hook runtime is not a qualified persistent installation (supported: a persistent npm " +
|
|
91
91
|
`install of ${MCP_PACKAGE_NAME} ${HOOK_RUNTIME_COMPATIBLE_RANGE} on macOS or Linux with an even ` +
|
|
92
|
-
`Node LTS line >= ${HOOK_RUNTIME_MIN_NODE_MAJOR}). See mcp_server/docs/
|
|
92
|
+
`Node LTS line >= ${HOOK_RUNTIME_MIN_NODE_MAJOR}). See mcp_server/docs/EPIC-IMPLEMENTER.md.`,
|
|
93
93
|
"node-unavailable": "`node` could not be launched from PATH, so the hook cannot run. Put a qualified Node on PATH " +
|
|
94
94
|
"and re-run `epic-implementer doctor`.",
|
|
95
95
|
"native-unavailable": "better-sqlite3 does not load for the hook's Node, so no event can be recorded. Reinstall the " +
|
|
@@ -23,14 +23,13 @@
|
|
|
23
23
|
* inspected a local checkout to decide merge eligibility would be answering a
|
|
24
24
|
* different question from the one the server's before-merge preflight answers.
|
|
25
25
|
*
|
|
26
|
-
* ##
|
|
26
|
+
* ## Remedy command
|
|
27
27
|
*
|
|
28
|
-
* {@link MERGE_WORKFLOW_REMEDY_COMMAND}
|
|
29
|
-
*
|
|
30
|
-
* yet an executable entry point on the operator's machine. That residual risk,
|
|
31
|
-
* its three containments, and who owns escalation are recorded once, in the
|
|
28
|
+
* {@link MERGE_WORKFLOW_REMEDY_COMMAND} is the complete generate-and-PR command that
|
|
29
|
+
* BAPI-1136 shipped (BAPI-1376 made it a full launcher command). Its history and containments are recorded once, in the
|
|
32
30
|
* Python module's docstring; this mirror does not restate them.
|
|
33
31
|
*/
|
|
32
|
+
import { MCP_PACKAGE_NAME } from "../mcp-identity.js";
|
|
34
33
|
/** The one `RunPolicy.merge_execution_mode` value this contract governs. */
|
|
35
34
|
export const MERGE_EXECUTION_MODE_GITHUB_ACTIONS = "github_actions";
|
|
36
35
|
/**
|
|
@@ -80,7 +79,27 @@ export function mergeWorkflowVersionSupportsIntegrationActions(version) {
|
|
|
80
79
|
INTEGRATION_ACTION_WORKFLOW_VERSIONS.includes(version));
|
|
81
80
|
}
|
|
82
81
|
/**
|
|
83
|
-
* The
|
|
84
|
-
* request.
|
|
82
|
+
* The dispatched subcommand that generates the merge workflow and opens its pull
|
|
83
|
+
* request. This is the part the CLI's own dispatcher (`index.ts`) matches on
|
|
84
|
+
* (`install`), not the launcher that starts it. The Python mirror derives its
|
|
85
|
+
* command head from this value.
|
|
85
86
|
*/
|
|
86
|
-
export const
|
|
87
|
+
export const MERGE_WORKFLOW_SUBCOMMAND = "install conductor --merge-workflow";
|
|
88
|
+
/**
|
|
89
|
+
* The complete, copy-ready command that generates the merge workflow and opens
|
|
90
|
+
* its pull request. Every refusal, remediation, and readiness line imports this
|
|
91
|
+
* constant.
|
|
92
|
+
*
|
|
93
|
+
* The package name comes from `mcp-identity.ts`, the sole source of that
|
|
94
|
+
* literal (BAPI-806). That module is itself dependency-free, so this import can
|
|
95
|
+
* never close a cycle. The `<@org/team|@user>` placeholder is deliberately NOT a valid owner — the
|
|
96
|
+
* installer refuses it — so an operator must replace it before running.
|
|
97
|
+
*/
|
|
98
|
+
export const MERGE_WORKFLOW_REMEDY_COMMAND = `npx -y ${MCP_PACKAGE_NAME} ${MERGE_WORKFLOW_SUBCOMMAND} --codeowner <@org/team|@user>`;
|
|
99
|
+
/**
|
|
100
|
+
* One reusable instruction to print beside {@link MERGE_WORKFLOW_REMEDY_COMMAND}.
|
|
101
|
+
* Bridge cannot infer who should review the workflow file, so the placeholder
|
|
102
|
+
* must be replaced by hand.
|
|
103
|
+
*/
|
|
104
|
+
export const MERGE_WORKFLOW_CODEOWNER_INSTRUCTION = "Bridge cannot infer the code owner. Before running, replace <@org/team|@user> " +
|
|
105
|
+
"with the one @user or @org/team who should review the workflow file.";
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
* not eligible: it is a workflow this build has never been taught to reason
|
|
32
32
|
* about, and "newer is probably fine" is a guess about a merge actuator.
|
|
33
33
|
*/
|
|
34
|
-
import { MERGE_WORKFLOW_CHILD_MERGE_COMPATIBLE_VERSIONS, MERGE_WORKFLOW_REMEDY_COMMAND, mergeWorkflowVersionSupportsChildMerge, } from "./merge-execution-contract.js";
|
|
34
|
+
import { MERGE_WORKFLOW_CHILD_MERGE_COMPATIBLE_VERSIONS, MERGE_WORKFLOW_CODEOWNER_INSTRUCTION, MERGE_WORKFLOW_REMEDY_COMMAND, mergeWorkflowVersionSupportsChildMerge, } from "./merge-execution-contract.js";
|
|
35
35
|
/**
|
|
36
36
|
* Remediation for a verdict that could not be established.
|
|
37
37
|
*
|
|
@@ -51,7 +51,8 @@ export const MERGE_EXECUTION_UNVERIFIABLE_REMEDIATION = "re-run once the Bridge
|
|
|
51
51
|
* second way.
|
|
52
52
|
*/
|
|
53
53
|
export const MERGE_EXECUTION_INELIGIBLE_REMEDIATION = `run \`${MERGE_WORKFLOW_REMEDY_COMMAND}\` to generate the merge workflow and open its ` +
|
|
54
|
-
|
|
54
|
+
`pull request. ${MERGE_WORKFLOW_CODEOWNER_INSTRUCTION} ` +
|
|
55
|
+
"Then re-run once it is merged to the default branch.";
|
|
55
56
|
function unverifiable(reason, detail) {
|
|
56
57
|
return {
|
|
57
58
|
status: "could_not_verify",
|
|
@@ -531,7 +531,7 @@ export async function observePrCiFromPollResponse(commitRef, pollResponse, deps
|
|
|
531
531
|
function extractTicketKeyFromRef(headRef) {
|
|
532
532
|
if (!headRef)
|
|
533
533
|
return null;
|
|
534
|
-
const match = /([A-Z][A-Z0-
|
|
534
|
+
const match = /([A-Z][A-Z0-9_]{0,19}-\d+)/i.exec(headRef);
|
|
535
535
|
return match ? match[1].toUpperCase() : null;
|
|
536
536
|
}
|
|
537
537
|
/**
|
|
@@ -39,6 +39,8 @@ import { readinessEndpointEnvironments, resolveConductorBridgeApiAccess, resolve
|
|
|
39
39
|
import { validateHttpBaseUrl } from "../base-url.js";
|
|
40
40
|
import { claudeReviewWorkflowPath } from "../claude-review-workflow.js";
|
|
41
41
|
import { runDefaultPlanePreflight } from "../plane/cli.js";
|
|
42
|
+
import { planeNotApplicableForTarget } from "../plane/endpoint.js";
|
|
43
|
+
import { resolvePlaneServerEndpoint } from "../plane/member-roster.js";
|
|
42
44
|
/** Fixed notice emitted to stderr when a source could not be collected. */
|
|
43
45
|
export const READINESS_COLLECTION_NOTICE = "one or more readiness sources could not be collected; the affected prerequisites are reported as failures.";
|
|
44
46
|
/** Usage text for `epic-implementer readiness`. */
|
|
@@ -101,10 +103,18 @@ async function collectProductionReport(repoRoot, env, baseUrl) {
|
|
|
101
103
|
const accessResult = baseUrl === undefined
|
|
102
104
|
? await resolveConductorBridgeApiAccess({ env: reportEnv, cwd: repoRoot })
|
|
103
105
|
: await resolveEpicLaunchBridgeApiAccess({ baseUrl }, { env: resolverEnv, cwd: repoRoot });
|
|
106
|
+
// BAPI-1360: the plane checks apply only when the selected access target names
|
|
107
|
+
// the local plane. Resolved from the SAME env the plane preflight reads, through
|
|
108
|
+
// the shared leaf predicate `drive-epic` uses, and never guessed: a target that
|
|
109
|
+
// is remote, or that cannot be resolved at all, is an explicit skip.
|
|
110
|
+
const planeNotApplicable = planeNotApplicableForTarget(resolvePlaneServerEndpoint(reportEnv), accessResult.ok ? accessResult.access.baseUrl : null);
|
|
104
111
|
if (baseUrl !== undefined && !accessResult.ok) {
|
|
105
112
|
// A refused override is never permission for a downstream doctor collector
|
|
106
113
|
// to resolve credentials afresh from the general CLI environment.
|
|
107
|
-
return collectConductorReadinessGate({
|
|
114
|
+
return collectConductorReadinessGate({
|
|
115
|
+
repoName: null,
|
|
116
|
+
...(planeNotApplicable !== null ? { planeNotApplicable } : {}),
|
|
117
|
+
});
|
|
108
118
|
}
|
|
109
119
|
const access = accessResult.ok ? accessResult.access : null;
|
|
110
120
|
return collectConductorReadinessGate({
|
|
@@ -134,7 +144,13 @@ async function collectProductionReport(repoRoot, env, baseUrl) {
|
|
|
134
144
|
// The deny-enforcement probe is never spawned from this command; the check
|
|
135
145
|
// is reported as an explicit skip naming `epic-implementer doctor` instead.
|
|
136
146
|
conductorDoctorDeps: { env: reportEnv, skipDenyProbe: true },
|
|
137
|
-
|
|
147
|
+
// Provided ONLY for a target that names the local plane; otherwise the rows
|
|
148
|
+
// are skipped without any probe.
|
|
149
|
+
...(planeNotApplicable === null
|
|
150
|
+
? {
|
|
151
|
+
runPlanePreflight: () => runDefaultPlanePreflight(repoRoot, reportEnv),
|
|
152
|
+
}
|
|
153
|
+
: { planeNotApplicable }),
|
|
138
154
|
});
|
|
139
155
|
}
|
|
140
156
|
/**
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
* The one exception is {@link inspectWorkflowPresence}, which awaits the
|
|
34
34
|
* READ-ONLY reader seams its caller injects and never opens a path of its own.
|
|
35
35
|
*/
|
|
36
|
+
import { PRINTED_CONNECT_GITHUB_COMMAND } from "../printed-remedy-commands.js";
|
|
36
37
|
import { MANAGED_HOOK_NAMES } from "./git-hooks.js";
|
|
37
38
|
import { MCP_PACKAGE_NAME } from "../mcp-identity.js";
|
|
38
39
|
import { CLAUDE_REVIEW_WORKFLOW_DRIFT_REMEDIATION, summarizeClaudeReviewWorkflowDrift, } from "../claude-review-workflow-drift.js";
|
|
@@ -41,7 +42,7 @@ import { classifyMergeExecutionEligibility, MERGE_EXECUTION_UNVERIFIABLE_REMEDIA
|
|
|
41
42
|
import { MERGE_WORKFLOW_CHILD_MERGE_COMPATIBLE_VERSIONS, MERGE_WORKFLOW_PATH, mergeWorkflowVersionSupportsChildMerge, } from "./merge-execution-contract.js";
|
|
42
43
|
import { classifyDatabaseMigration, classifySingleTransitionAuthority, classifyWorkerPresent, } from "./bring-up-facts.js";
|
|
43
44
|
/** Operator runbook pointer surfaced beside the executor gap. */
|
|
44
|
-
export const CONDUCTOR_OPERATOR_RUNBOOK_POINTER = "docs/
|
|
45
|
+
export const CONDUCTOR_OPERATOR_RUNBOOK_POINTER = "docs/epic-implementer/operator-runbook.md";
|
|
45
46
|
/**
|
|
46
47
|
* Capability-matrix guidance for the executor leg.
|
|
47
48
|
*
|
|
@@ -105,6 +106,22 @@ export const REQUIRED_CHECKS_PRESENTATION_COPY = {
|
|
|
105
106
|
remediation: "restore required-check verification (a readable default branch with static lane " +
|
|
106
107
|
"workflows and a current Bridge API), or correct the required-check configuration.",
|
|
107
108
|
},
|
|
109
|
+
// BAPI-1360 — file- and check-specific copy for the two proven, repository-admin
|
|
110
|
+
// causes of `unverifiable`. Fixed strings over the two hard-coded workflow paths
|
|
111
|
+
// and the two gate names; nothing from the payload is interpolated.
|
|
112
|
+
retiredGate: {
|
|
113
|
+
detail: "Could not verify — a configured required check is the retired gate " +
|
|
114
|
+
`conductor-ci / gate; ${REQUIRED_CHECKS_OPERATOR_HOLD}`,
|
|
115
|
+
remediation: "a repository administrator replaces conductor-ci / gate with epic-implementer-ci / gate in the " +
|
|
116
|
+
"plan's required_checks, the supervisor done-gate checks, and branch protection.",
|
|
117
|
+
},
|
|
118
|
+
laneAbsent: {
|
|
119
|
+
detail: "Could not verify — a lane workflow file is missing from the default branch, so a configured " +
|
|
120
|
+
`required check has no producer; ${REQUIRED_CHECKS_OPERATOR_HOLD}`,
|
|
121
|
+
remediation: "a repository administrator adds the missing lane workflow file(s) — " +
|
|
122
|
+
".github/workflows/epic-implementer-ci.yml and/or .github/workflows/claude-review.yml — " +
|
|
123
|
+
"to the default branch so the configured required checks (such as epic-implementer-ci / gate) have a producer.",
|
|
124
|
+
},
|
|
108
125
|
empty: {
|
|
109
126
|
detail: "Could not verify — the required-check list is EMPTY, which is unsafe: the done gate " +
|
|
110
127
|
`would pass unconditionally; ${REQUIRED_CHECKS_OPERATOR_HOLD}`,
|
|
@@ -155,6 +172,24 @@ export function presentRequiredChecks(supervisor) {
|
|
|
155
172
|
...REQUIRED_CHECKS_PRESENTATION_COPY.unknown_name,
|
|
156
173
|
};
|
|
157
174
|
}
|
|
175
|
+
// BAPI-1360 — optional bounded facts refine the generic unverifiable copy. An
|
|
176
|
+
// old server reports neither, and keeps the wording above. Retired gate ranks
|
|
177
|
+
// first, then a proven-absent lane; anything else stays generic.
|
|
178
|
+
if (supervisor.required_checks_retired_gate_configured === true) {
|
|
179
|
+
return {
|
|
180
|
+
outcome: "unverifiable",
|
|
181
|
+
satisfied: false,
|
|
182
|
+
...REQUIRED_CHECKS_PRESENTATION_COPY.retiredGate,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
if (supervisor.required_checks_ci_workflow_absent === true ||
|
|
186
|
+
supervisor.required_checks_review_workflow_absent === true) {
|
|
187
|
+
return {
|
|
188
|
+
outcome: "unverifiable",
|
|
189
|
+
satisfied: false,
|
|
190
|
+
...REQUIRED_CHECKS_PRESENTATION_COPY.laneAbsent,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
158
193
|
return {
|
|
159
194
|
outcome: "unverifiable",
|
|
160
195
|
satisfied: false,
|
|
@@ -268,7 +303,7 @@ export function githubCredentialsSection(readiness) {
|
|
|
268
303
|
detail: g.credentials_readable
|
|
269
304
|
? `incomplete: ${missing} unresolved`
|
|
270
305
|
: "no GitHub credential row for this repository",
|
|
271
|
-
remediation:
|
|
306
|
+
remediation: `connect GitHub from the Bridge setup UI or run \`${PRINTED_CONNECT_GITHUB_COMMAND}\`.`,
|
|
272
307
|
};
|
|
273
308
|
}
|
|
274
309
|
export function githubActionsSection(readiness) {
|
|
@@ -396,7 +431,7 @@ export function reconcilerSection(readiness) {
|
|
|
396
431
|
// remediation at all — so a report could name a gap with no next step.
|
|
397
432
|
return {
|
|
398
433
|
id: "reconciler-liveness",
|
|
399
|
-
label: "
|
|
434
|
+
label: "Active-run progress",
|
|
400
435
|
status: "degraded",
|
|
401
436
|
detail: "liveness could not be read",
|
|
402
437
|
remediation: "retry once the Bridge API is reachable; reconciler liveness is unknown here, " +
|
|
@@ -406,19 +441,19 @@ export function reconcilerSection(readiness) {
|
|
|
406
441
|
if (!r.stale) {
|
|
407
442
|
return {
|
|
408
443
|
id: "reconciler-liveness",
|
|
409
|
-
label: "
|
|
444
|
+
label: "Active-run progress",
|
|
410
445
|
status: "ok",
|
|
411
446
|
detail: `last tick ${r.last_tick_age_seconds}s ago across ${r.active_run_count} active run(s)`,
|
|
412
447
|
};
|
|
413
448
|
}
|
|
414
449
|
return {
|
|
415
450
|
id: "reconciler-liveness",
|
|
416
|
-
label: "
|
|
451
|
+
label: "Active-run progress",
|
|
417
452
|
status: "degraded",
|
|
418
453
|
detail: r.last_tick_at === null
|
|
419
454
|
? "no reconciler tick recorded for this repository"
|
|
420
455
|
: `last tick ${r.last_tick_age_seconds}s ago (threshold ${readiness.thresholds.reconciler_stale_after_seconds}s)`,
|
|
421
|
-
remediation: "
|
|
456
|
+
remediation: "check the worker first (`server.worker-present`), then inspect the stalled run with `epic-implementer epic-status`.",
|
|
422
457
|
};
|
|
423
458
|
}
|
|
424
459
|
// ---------------------------------------------------------------------------
|