@bridge_gpt/mcp-server 0.2.41 → 0.2.42

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.
Files changed (73) hide show
  1. package/README.md +10 -10
  2. package/build/agent-capabilities/cli.js +2 -1
  3. package/build/agent-launchers/claude-executor-adapter.js +17 -4
  4. package/build/claude-user-config-doctor.js +42 -11
  5. package/build/cli-release.js +2 -1
  6. package/build/commands.generated.js +4 -4
  7. package/build/conduct-epic/bridge-client.js +354 -113
  8. package/build/conduct-epic/checkpoint-store.js +17 -0
  9. package/build/conduct-epic/cli.js +752 -99
  10. package/build/conduct-epic/cut-protocol.js +327 -0
  11. package/build/conduct-epic/spawn.js +14 -2
  12. package/build/conductor/bridge-api-client.js +27 -1
  13. package/build/conductor/cli.js +46 -1
  14. package/build/conductor/doctor.js +101 -16
  15. package/build/conductor/epic-reconcile.js +72 -19
  16. package/build/conductor/epic-runtime.js +15 -3
  17. package/build/conductor/errors.js +47 -0
  18. package/build/conductor/git-hooks.js +205 -11
  19. package/build/conductor/install-doctor.js +230 -1
  20. package/build/conductor/local-merge.js +130 -28
  21. package/build/conductor/tools.js +32 -3
  22. package/build/conductor/worker-ledger-cli.js +27 -1
  23. package/build/conductor-bin.js +15 -15
  24. package/build/credentials-cli.js +3 -2
  25. package/build/doctor.js +107 -41
  26. package/build/executor/cli.js +48 -1
  27. package/build/executor/env.js +21 -0
  28. package/build/executor/index-scope.js +39 -0
  29. package/build/executor/job-log-registry.js +69 -0
  30. package/build/executor/job-runner.js +148 -26
  31. package/build/executor/live-worker-registry.js +83 -0
  32. package/build/executor/observation.js +167 -6
  33. package/build/executor/platform.js +147 -3
  34. package/build/executor/process.js +58 -14
  35. package/build/executor/runner.js +235 -48
  36. package/build/executor/test-clock.js +3 -2
  37. package/build/index-scope-contract.js +96 -0
  38. package/build/index.js +153 -204
  39. package/build/init.js +83 -22
  40. package/build/install-bridge-conductor.js +323 -14
  41. package/build/install-bridge.js +202 -38
  42. package/build/install-doctor.js +23 -9
  43. package/build/install-reexec.js +2 -1
  44. package/build/launcher-config-inspection.js +83 -22
  45. package/build/mcp-host-config.js +331 -67
  46. package/build/mcp-host-targets.js +45 -21
  47. package/build/mcp-identity.js +92 -0
  48. package/build/mcp-install-state.js +94 -1
  49. package/build/mcp-invoke.js +2 -1
  50. package/build/mcp-provisioning.js +45 -12
  51. package/build/mcp-registration-doctor.js +35 -13
  52. package/build/mcp-server-invocation.js +4 -2
  53. package/build/merge-pull-request.js +208 -9
  54. package/build/pipelines.generated.js +3 -3
  55. package/build/plane/defaults.js +4 -1
  56. package/build/plane/preflight.js +81 -10
  57. package/build/plane/test-fakes.js +9 -1
  58. package/build/readme.generated.js +1 -1
  59. package/build/regression-check.js +3 -2
  60. package/build/review-tickets.js +8 -7
  61. package/build/run-unit-tests-launcher.js +74 -1
  62. package/build/schedule-run.js +3 -2
  63. package/build/setup-epic.js +453 -78
  64. package/build/sfcc/tool-wrapper.js +15 -0
  65. package/build/start-tickets-prereqs.js +11 -6
  66. package/build/start-tickets.js +91 -85
  67. package/build/update-check.js +3 -2
  68. package/build/upgrade-advice.js +2 -1
  69. package/build/upgrade-cli.js +50 -18
  70. package/build/version.generated.js +1 -1
  71. package/docs/CONDUCTOR.md +22 -0
  72. package/docs/install/mcp-tool-integrations.md +19 -3
  73. package/package.json +2 -2
@@ -30,6 +30,31 @@
30
30
  * file-overlap serialization, so a divergence is legitimate, not corruption.
31
31
  * 5. Store, then approve.
32
32
  *
33
+ * BAPI-850 — a FEATURE-BRANCH run (`policy_json.base_branch` set, whether by
34
+ * `--feature-branch` or by `--policy-file`) performs the full scope-bearing
35
+ * start sequence in this one invocation, in this order:
36
+ *
37
+ * a. Read canonical parse truth (`GET /jira/parse-status`). No succeeded
38
+ * parse or no indexed commit → exit 1 with "parse the repository first";
39
+ * no run is created and no branch is changed.
40
+ * b. Create the run as today (step 3) — FIRST, because a scope's run
41
+ * association is immutable server-side and can never be attached later.
42
+ * c. Cut or validate `origin/<feature>` at EXACTLY the canonical indexed SHA
43
+ * with the operator's LOCAL `git`, through the shared cut protocol
44
+ * `conduct-epic init` also drives, with the run id so the scope is minted
45
+ * owned by the run. An existing branch at a different commit fails closed.
46
+ * The GitHub App never pushes — it is `contents: read`.
47
+ * d. Store and approve (unchanged requests). The server now READ-validates the
48
+ * ref against the scope's recorded cut and schedules the seed + verifying
49
+ * parse in the background; the run goes `active` immediately.
50
+ * e. Poll the scope's lifecycle with a bounded wait, reporting
51
+ * `Provisioning → Seeding → Verifying → Ready`. `failed` and a timeout
52
+ * both exit 1; the reconciler's dispatch-freshness hold keeps an
53
+ * un-ready scope from dispatching either way.
54
+ *
55
+ * A run with no feature branch is byte-identical to before: no parse read, no
56
+ * `git`, no scope, no polling, no extra JSON keys.
57
+ *
33
58
  * Read-only against the local filesystem: it reads the plan sidecar and the
34
59
  * optional policy file, and writes nothing.
35
60
  */
@@ -38,47 +63,24 @@ import os from "node:os";
38
63
  import readline from "node:readline";
39
64
  import { approveEpicPlan, createEpicRunWithDisposition, fetchEpicRunState, replaceEpicRunPolicy, resolveConductorBridgeApiAccess, storeEpicPlan, validateEpicPlan, ConductorBridgeApiError, } from "./conductor/bridge-api-client.js";
40
65
  import { validateBranchName } from "./base-ref.js";
41
- // BAPI-814/E1a: reused from the conduct-epic client on purpose. The override is
42
- // ONE repository-wide row with ONE reader; adding a second wrapper under
66
+ // Reused from the conduct-epic client on purpose: adding a second wrapper under
43
67
  // `conductor/` would give the v2 path its own copy to drift from.
44
- import { getIndexBranch } from "./conduct-epic/bridge-client.js";
68
+ //
69
+ // BAPI-850 extends that reuse to the canonical parse read, the repository base
70
+ // branch, the per-scope status/lifecycle reads, and the exact-cut protocol: the
71
+ // typed clients the pilot already has are the v2 path's clients too, so neither
72
+ // conductor can drift on a field name or an endpoint shape. No MCP tool, no
73
+ // input-schema field, and no description changes.
74
+ import { getConfigFieldBaseBranch, getIndexScopeLifecycle, getParseStatus, } from "./conduct-epic/bridge-client.js";
75
+ import { createExecFileRunCommand, ensureCommitResolvableLocally, normalizeCommitSha, performExactIndexScopeCut, pollIndexScopeLifecycle, readRemoteBranchHead, runGit, SCOPE_LIFECYCLE_LABELS, } from "./conduct-epic/cut-protocol.js";
45
76
  import { hashPlan } from "./conductor/plan.js";
77
+ import { MCP_PACKAGE_NAME } from "./mcp-identity.js";
46
78
  /** Accepted `policy_json.review_policy.source` values (the `ReviewPolicy` surface). */
47
79
  export const SETUP_EPIC_REVIEW_POLICY_SOURCES = [
48
80
  "verdict_protocol",
49
81
  "native_review_decision",
50
82
  "none",
51
83
  ];
52
- /**
53
- * The warning lines for an active repository-wide index-branch override, or an
54
- * empty array when there is nothing to say (BAPI-814/E1a).
55
- *
56
- * Fail-open by construction. This is optional enrichment layered onto a command
57
- * whose real job is creating an epic run, so an unreadable override must not
58
- * become a preflight failure: an unavailable lookup produces one bounded,
59
- * sanitized diagnostic and `setup-epic` continues. The client already returns a
60
- * sanitized `error` string and never the raw service body, so nothing further
61
- * needs stripping here.
62
- */
63
- async function describeActiveIndexOverride(access, deps) {
64
- const result = await getIndexBranch(access, deps.fetch);
65
- if (!result.ok) {
66
- return [
67
- ` [warn] The repository's indexed-branch override could not be read: ${result.error}. ` +
68
- `Continuing — this is optional context, not a prerequisite.`,
69
- ];
70
- }
71
- const override = result.value.override;
72
- if (override === null)
73
- return [];
74
- return [
75
- ` [warn] ${access.repoName} has an ACTIVE index-branch override: ` +
76
- `'${override.override_branch}'. Repository parses currently index that branch, ` +
77
- `and the base-branch config field this run may fall back on resolves through the ` +
78
- `same override.`,
79
- ` Setup is CONTINUING — this is a warning, not a refusal.`,
80
- ];
81
- }
82
84
  /** Echoed single-line prompt on stderr (mirrors connect-github's helper). */
83
85
  function defaultPromptLine(promptText) {
84
86
  return new Promise((resolve) => {
@@ -111,6 +113,7 @@ export function createDefaultSetupEpicDeps() {
111
113
  errorLog: (m) => console.error(m),
112
114
  isTTY: Boolean(process.stdin.isTTY),
113
115
  promptLine: defaultPromptLine,
116
+ runCommand: createExecFileRunCommand(),
114
117
  };
115
118
  }
116
119
  /** User-facing usage text. */
@@ -129,10 +132,14 @@ export function getSetupEpicUsage() {
129
132
  " --repo <name> Repo name (default: BAPI_REPO_NAME or .bridge/config)",
130
133
  " --plan-version <n> Assert the sidecar's plan_version equals <n>",
131
134
  " --feature-branch <name> Run the epic on a dedicated feature branch.",
132
- " The branch is created from the repository base branch",
133
- " on origin, and every child-ticket PR targets it.",
134
- " Omit (the default) to continue on the repository base",
135
- " branch. Interactive runs are offered a proposal.",
135
+ " setup-epic cuts the branch on origin with your LOCAL git",
136
+ " at exactly the repository's canonical indexed commit",
137
+ " (the repository must be parsed first), the server",
138
+ " validates it, and every child-ticket PR targets it.",
139
+ " setup-epic then waits (bounded) for the run's index",
140
+ " scope to become ready. Omit (the default) to continue",
141
+ " on the repository base branch. Interactive runs are",
142
+ " offered a proposal.",
136
143
  " --review-policy <src> PER-RUN review policy source, one of:",
137
144
  ` ${SETUP_EPIC_REVIEW_POLICY_SOURCES.join(", ")}.`,
138
145
  " Composed into policy_json.review_policy on create.",
@@ -170,7 +177,7 @@ export function getSetupEpicUsage() {
170
177
  "",
171
178
  "After setup, the server-side reconciler picks the run up within ~30s.",
172
179
  "To execute claimed jobs on this machine, run:",
173
- " npx -y @bridge_gpt/mcp-server executor --repo <name>",
180
+ ` npx -y ${MCP_PACKAGE_NAME} executor --repo <name>`,
174
181
  ].join("\n");
175
182
  }
176
183
  function takeValue(argv, i, flag) {
@@ -897,6 +904,144 @@ function emitRefusal(deps, opts, result) {
897
904
  deps.log(JSON.stringify(result, null, 2));
898
905
  return 1;
899
906
  }
907
+ // ---------------------------------------------------------------------------
908
+ // Feature-branch start sequence helpers (BAPI-850)
909
+ // ---------------------------------------------------------------------------
910
+ /**
911
+ * The one recovery action for a scope that the server recorded as `failed`.
912
+ *
913
+ * Approval is idempotent and the server re-drives a `failed` scope's seed and
914
+ * verification when it is re-approved, so re-running the SAME `setup-epic`
915
+ * invocation is the canonical fix. The `canonical_index_advanced` category is
916
+ * the exception the pilot also calls out: the cut itself must be re-driven at
917
+ * the newer commit.
918
+ */
919
+ export const SETUP_EPIC_SCOPE_RECOVERY_COMMAND = "re-run this same setup-epic command (approval is idempotent and re-drives the scope's seed and verification)";
920
+ /** The lead sentence of every fail-closed canonical-parse refusal (BAPI-850). */
921
+ export const SETUP_EPIC_PARSE_FIRST_MESSAGE = "parse the repository first";
922
+ /**
923
+ * Resolve the EFFECTIVE feature branch after policy composition.
924
+ *
925
+ * `--feature-branch` wins when given. Otherwise a valid `policy_json.base_branch`
926
+ * supplied by `--policy-file` names the branch — the file ALREADY answered the
927
+ * question, and the run will be created with that branch whether or not this
928
+ * CLI noticed. A malformed or unknown policy field (including the camelCase
929
+ * `baseBranch` slip) is deliberately NOT interpreted here: the server's strict
930
+ * `RunPolicy` validation owns rejecting it, so this returns `undefined` and the
931
+ * request proceeds to that 422 unchanged.
932
+ */
933
+ export function resolveEffectiveFeatureBranch(featureBranch, requestedPolicy) {
934
+ if (featureBranch !== undefined)
935
+ return featureBranch;
936
+ const raw = requestedPolicy?.["base_branch"];
937
+ if (typeof raw !== "string")
938
+ return undefined;
939
+ const trimmed = raw.trim();
940
+ if (trimmed === "" || validateBranchName(trimmed) !== null)
941
+ return undefined;
942
+ return trimmed;
943
+ }
944
+ /**
945
+ * Read canonical parse truth and require a `succeeded` parse with a full
946
+ * indexed commit — the commit `origin/<feature>` will be cut at.
947
+ *
948
+ * Fails CLOSED (ratified R-2). The first line of every refusal is the exact
949
+ * operator instruction; the closing lines state what did NOT happen. An
950
+ * UNREADABLE status is kept distinct from an unsuccessful one — "could not read"
951
+ * is not evidence the repository is unindexed, and telling that operator to
952
+ * re-index would send them to re-parse a repository that may be fine.
953
+ */
954
+ export async function readCanonicalParseTruth(access, fetchImpl) {
955
+ const notCreated = [
956
+ "No run was created and no branch was changed.",
957
+ ];
958
+ const parse = await getParseStatus(access, fetchImpl);
959
+ if (!parse.ok) {
960
+ return {
961
+ ok: false,
962
+ lines: [
963
+ `The canonical parse status for ${access.repoName} could not be read: ${parse.error}. ` +
964
+ `Retry; if the repository has never been parsed, ${SETUP_EPIC_PARSE_FIRST_MESSAGE}.`,
965
+ ...notCreated,
966
+ ],
967
+ };
968
+ }
969
+ if (parse.value.status !== "succeeded") {
970
+ return {
971
+ ok: false,
972
+ lines: [
973
+ `${SETUP_EPIC_PARSE_FIRST_MESSAGE}: the canonical index for ${access.repoName} has no ` +
974
+ `successful parse (status: ${String(parse.value.status)}), so there is no commit to ` +
975
+ `cut the feature branch at.`,
976
+ ...notCreated,
977
+ ],
978
+ };
979
+ }
980
+ const indexed = normalizeCommitSha(parse.value.indexed_commit_sha);
981
+ if (indexed === null) {
982
+ return {
983
+ ok: false,
984
+ lines: [
985
+ `${SETUP_EPIC_PARSE_FIRST_MESSAGE}: the canonical index for ${access.repoName} published ` +
986
+ `no commit for its last successful parse, so there is no commit to cut the feature ` +
987
+ `branch at.`,
988
+ ...notCreated,
989
+ ],
990
+ };
991
+ }
992
+ return { ok: true, cutCommitSha: indexed };
993
+ }
994
+ /** The strict subset of `SetupEpicDeps` the shared cut protocol consumes. */
995
+ function cutProtocolDeps(deps, runCommand) {
996
+ return {
997
+ runCommand,
998
+ cwd: deps.cwd,
999
+ fetchImpl: deps.fetch,
1000
+ errorLog: deps.errorLog,
1001
+ sleep: deps.sleep,
1002
+ };
1003
+ }
1004
+ /**
1005
+ * Resolve the run's scope id for a REUSED, already-activated run, whose cut was
1006
+ * recorded by an earlier invocation and must not be re-driven.
1007
+ *
1008
+ * Reads the repository's lifecycle listing (an existing read) and matches on the
1009
+ * server-recorded `epic_run_id`. Returns `null` when no scope is associated —
1010
+ * the server's approval reports that case itself.
1011
+ */
1012
+ async function resolveScopeIdForRun(access, fetchImpl, epicRunId) {
1013
+ const listing = await getIndexScopeLifecycle(access, fetchImpl);
1014
+ if (!listing.ok)
1015
+ return null;
1016
+ const match = listing.value.scopes.find((scope) => scope.epic_run_id === epicRunId);
1017
+ return match?.scope_id ?? null;
1018
+ }
1019
+ /** Operator-facing guidance for a bounded scope failure category. */
1020
+ function scopeFailureGuidance(reason, featureBranch) {
1021
+ if (reason === "canonical_index_advanced") {
1022
+ return (`The canonical index advanced before the scope could be seeded. The cut must be ` +
1023
+ `re-driven at the newer commit: delete origin/${featureBranch}, abandon this run ` +
1024
+ `(PATCH /jira/epic-runs/runs/<epic_run_id> {"status": "abandoned"}), and re-run setup-epic.`);
1025
+ }
1026
+ return `Recovery: ${SETUP_EPIC_SCOPE_RECOVERY_COMMAND}.`;
1027
+ }
1028
+ /**
1029
+ * Report one newly observed lifecycle transition, once, in order.
1030
+ *
1031
+ * Prior lines stay on screen as confirmation — no spinner rewrite, no
1032
+ * percentages. Branch, SHA, and scope identifiers are secondary technical
1033
+ * detail: the label leads, the scope id never does, and no credential appears.
1034
+ */
1035
+ function describeScopeTransition(state, status) {
1036
+ const label = SCOPE_LIFECYCLE_LABELS[state] ?? state;
1037
+ if (state === "ready") {
1038
+ return `Scope: ${label} — indexed at ${status.indexed_commit_sha ?? "unknown"} (scope ${status.scope_id})`;
1039
+ }
1040
+ if (state === "failed") {
1041
+ return `Scope: ${label} — ${status.last_error ?? "unknown"} (scope ${status.scope_id})`;
1042
+ }
1043
+ return `Scope: ${label}…`;
1044
+ }
900
1045
  export async function runSetupEpicCli(argv, overrides = {}) {
901
1046
  const deps = { ...createDefaultSetupEpicDeps(), ...overrides };
902
1047
  const parsed = parseSetupEpicArgs(argv);
@@ -1019,21 +1164,6 @@ export async function runSetupEpicCli(argv, overrides = {}) {
1019
1164
  say(`Local hash: ${localHash}`);
1020
1165
  for (const w of warnings)
1021
1166
  say(` [warn] ${w}`);
1022
- // --- Active index-branch override (BAPI-814/E1a) -------------------------
1023
- // Non-mutating enrichment, placed once credentials and repository identity are
1024
- // both known and before any run-state read or mutating request.
1025
- //
1026
- // WARNING ONLY, never a refusal. The override is repository-wide: while it is
1027
- // active every parse indexes that branch, and the create-PR config-field
1028
- // fallback and feature-branch provisioning read the same overridden
1029
- // `config_code_repositories.base_branch`. A v2 operator setting up an epic in
1030
- // that repository needs to know, but the condition is legitimate — it is
1031
- // usually another epic mid-flight — so setup continues.
1032
- //
1033
- // Emitted in `--dry-run` too: the condition already exists in the repository
1034
- // and is independent of whether this invocation writes anything.
1035
- for (const line of await describeActiveIndexOverride(access, deps))
1036
- say(line);
1037
1167
  // --- Feature branch selection (BAPI-655) --------------------------------
1038
1168
  // Resolved AFTER local/access context is known but BEFORE any run-state read
1039
1169
  // or mutating request, so malformed interactive input fails before network
@@ -1041,7 +1171,8 @@ export async function runSetupEpicCli(argv, overrides = {}) {
1041
1171
  const promptedInteractively = opts.featureBranch === undefined && !policyDeclaresBranch && deps.isTTY && !opts.json;
1042
1172
  const featureBranch = await resolveFeatureBranchSelection(opts, access.repoName, deps, policyDeclaresBranch);
1043
1173
  if (featureBranch !== undefined) {
1044
- say(`Feature: ${featureBranch} (create from repository base branch on origin)`);
1174
+ say(`Feature: ${featureBranch} (cut on origin with local git at the canonical indexed ` +
1175
+ `commit; validated server-side at approval)`);
1045
1176
  }
1046
1177
  else if (promptedInteractively) {
1047
1178
  // Ordinary, non-warning notice — only when an interactive operator declined.
@@ -1064,6 +1195,15 @@ export async function runSetupEpicCli(argv, overrides = {}) {
1064
1195
  requestedPolicy = composed.policy;
1065
1196
  say(`Policy: ${opts.policyFile} (complete run policy, applied at creation)`);
1066
1197
  }
1198
+ // --- Effective feature branch (BAPI-850) ----------------------------------
1199
+ // Derived AFTER policy composition so a branch the policy file declares under
1200
+ // `base_branch` is honored exactly as `--feature-branch` would be. A malformed
1201
+ // or unknown policy field is left for the server's strict validation.
1202
+ const effectiveFeatureBranch = resolveEffectiveFeatureBranch(featureBranch, requestedPolicy);
1203
+ if (effectiveFeatureBranch !== undefined && featureBranch === undefined) {
1204
+ say(`Feature: ${effectiveFeatureBranch} (from the policy file; cut on origin with local ` +
1205
+ `git at the canonical indexed commit; validated server-side at approval)`);
1206
+ }
1067
1207
  // --- Step 0: pre-check ---------------------------------------------------
1068
1208
  // Never create on an ambiguous read. A wrong answer here mints a duplicate run.
1069
1209
  let existingRunId = null;
@@ -1206,8 +1346,64 @@ export async function runSetupEpicCli(argv, overrides = {}) {
1206
1346
  if (validated) {
1207
1347
  say("Validation: server validation: passed");
1208
1348
  say(` plan hash ${validated.planHash}`);
1209
- say(` serialization ${validated.serializationEnabled ? "enabled" : "disabled"}, ` +
1210
- `${validated.insertedEdges} edge(s) inserted`);
1349
+ // BAPI-848 three distinct outcomes, never collapsed into success language.
1350
+ // "0 edges inserted" used to read as proof the siblings could not conflict.
1351
+ // It never was: serialization only sees paths BOTH nodes declared, and the
1352
+ // repo's worst conflict hotspot was one nobody declared.
1353
+ if (!validated.serializationEnabled) {
1354
+ say(" serialization disabled — no ordering was derived from file overlap");
1355
+ }
1356
+ else if (validated.insertedEdges === 0) {
1357
+ say(" serialization enabled — no declared file overlaps produced edges");
1358
+ }
1359
+ else {
1360
+ say(` serialization enabled — ${validated.insertedEdges} edge(s) inserted from ` +
1361
+ `${validated.overlappingPairsFound} overlapping pair(s)`);
1362
+ }
1363
+ if (validated.serializationEnabled) {
1364
+ say(` coverage: ${validated.coverageScope}`);
1365
+ if (validated.undeclaredNodes > 0 || validated.undeclaredPairsSkipped > 0) {
1366
+ deps.errorLog(`WARNING: ${validated.undeclaredNodes} node(s) declared no touched_files, so ` +
1367
+ `${validated.undeclaredPairsSkipped} pair(s) were never evaluated. Serialization ` +
1368
+ `proves ordering only for paths BOTH nodes declared — it cannot show that these ` +
1369
+ `tickets will avoid each other.`);
1370
+ }
1371
+ }
1372
+ }
1373
+ // --- Scope-bearing preflight (BAPI-850) ----------------------------------
1374
+ // A FEATURE-BRANCH run that has not yet activated must prove canonical parse
1375
+ // truth BEFORE any ref is changed or any run is created. A reused run that is
1376
+ // already active/blocked carries merged child work on its branch and a
1377
+ // recorded cut; it is never re-cut (its persisted run/scope association is
1378
+ // used for status instead). A no-feature run performs none of this.
1379
+ //
1380
+ // The scope's BASE is the repository's configured base branch (the same
1381
+ // resolution `conduct-epic init` uses), NOT `policy_json.base_branch` — that
1382
+ // key names the FEATURE branch. A feature branch equal to the configured base
1383
+ // needs no scope (dispatch already cuts from it) and matches the server's
1384
+ // long-standing no-op for that case.
1385
+ const existingRunActivated = existingStatus !== null &&
1386
+ existingStatus !== "planning" &&
1387
+ existingStatus !== "pending_approval";
1388
+ let scopeBaseBranch = null;
1389
+ let scopeBearing = false;
1390
+ let cutCommitSha = null;
1391
+ if (effectiveFeatureBranch !== undefined) {
1392
+ const configured = await getConfigFieldBaseBranch(access, deps.fetch);
1393
+ scopeBaseBranch =
1394
+ configured.ok && configured.value.base_branch !== null ? configured.value.base_branch : "main";
1395
+ scopeBearing = effectiveFeatureBranch !== scopeBaseBranch;
1396
+ }
1397
+ const needsCut = scopeBearing && !existingRunActivated;
1398
+ if (needsCut) {
1399
+ const truth = await readCanonicalParseTruth(access, deps.fetch);
1400
+ if (!truth.ok) {
1401
+ for (const line of truth.lines)
1402
+ deps.errorLog(line);
1403
+ return 1;
1404
+ }
1405
+ cutCommitSha = truth.cutCommitSha;
1406
+ say(`Cut: ${cutCommitSha} (canonical indexed commit; origin/${effectiveFeatureBranch} will be cut here)`);
1211
1407
  }
1212
1408
  if (opts.dryRun) {
1213
1409
  say("");
@@ -1220,8 +1416,10 @@ export async function runSetupEpicCli(argv, overrides = {}) {
1220
1416
  else {
1221
1417
  say(` - POST /jira/epic-runs/runs (create run for ${opts.epicKey})`);
1222
1418
  }
1223
- if (featureBranch !== undefined) {
1224
- say(` - feature branch: ${featureBranch} (create from repository base branch; no request made in dry-run)`);
1419
+ if (effectiveFeatureBranch !== undefined) {
1420
+ say(needsCut && cutCommitSha !== null
1421
+ ? ` - feature branch: ${effectiveFeatureBranch} (cut origin/${effectiveFeatureBranch} at canonical indexed commit ${cutCommitSha} with local git, then validate server-side; nothing cut, created, or seeded in dry-run)`
1422
+ : ` - feature branch: ${effectiveFeatureBranch} (validated server-side at approval; no request made in dry-run)`);
1225
1423
  }
1226
1424
  if (opts.reviewPolicy !== undefined) {
1227
1425
  say(` - review policy: ${opts.reviewPolicy} (per-run policy_json.review_policy; sets no repository default — use \`install-bridge conductor --review-policy\` for that)`);
@@ -1237,7 +1435,12 @@ export async function runSetupEpicCli(argv, overrides = {}) {
1237
1435
  local_plan_hash: localHash,
1238
1436
  existing_run_id: existingRunId,
1239
1437
  // Only present for a feature-branch run — no-feature JSON is unchanged.
1240
- ...(featureBranch !== undefined ? { feature_branch: featureBranch } : {}),
1438
+ // BAPI-850: the EFFECTIVE branch, so a policy-file-declared feature run
1439
+ // reports the same key a --feature-branch run does.
1440
+ ...(effectiveFeatureBranch !== undefined ? { feature_branch: effectiveFeatureBranch } : {}),
1441
+ // BAPI-850: the exact commit the cut WOULD use, reported without
1442
+ // cutting. Present only when the dry run validated canonical parse truth.
1443
+ ...(cutCommitSha !== null ? { cut_commit_sha: cutCommitSha } : {}),
1241
1444
  // Only present when a policy was explicitly selected — the
1242
1445
  // no-policy JSON shape is unchanged.
1243
1446
  ...(opts.reviewPolicy !== undefined
@@ -1252,6 +1455,12 @@ export async function runSetupEpicCli(argv, overrides = {}) {
1252
1455
  server_plan_hash: validated.planHash,
1253
1456
  serialization_enabled: validated.serializationEnabled,
1254
1457
  inserted_edges: validated.insertedEdges,
1458
+ // BAPI-848 — machine consumers get the same coverage the human
1459
+ // output carries, so neither can read 0 edges as a verdict.
1460
+ overlapping_pairs_found: validated.overlappingPairsFound,
1461
+ undeclared_nodes: validated.undeclaredNodes,
1462
+ undeclared_pairs_skipped: validated.undeclaredPairsSkipped,
1463
+ coverage_scope: validated.coverageScope,
1255
1464
  }
1256
1465
  : { result: "partial (local checks only)" }),
1257
1466
  warnings,
@@ -1259,6 +1468,46 @@ export async function runSetupEpicCli(argv, overrides = {}) {
1259
1468
  }
1260
1469
  return 0;
1261
1470
  }
1471
+ // --- Local git preflight for the cut (BAPI-850) --------------------------
1472
+ // Still BEFORE any mutating request: the operator's checkout must be able to
1473
+ // reach origin, hold the exact cut object, and origin/<feature> must be absent
1474
+ // or already at the cut. A failure here creates nothing and changes nothing.
1475
+ let cutRunCommand;
1476
+ if (needsCut && cutCommitSha !== null && scopeBaseBranch !== null) {
1477
+ cutRunCommand = deps.runCommand;
1478
+ if (cutRunCommand === undefined) {
1479
+ deps.errorLog(`A feature-branch run cuts origin/${effectiveFeatureBranch} with your local git, but no ` +
1480
+ `command runner is available in this environment. No run was created and no branch was changed.`);
1481
+ return 1;
1482
+ }
1483
+ const gitDeps = cutProtocolDeps(deps, cutRunCommand);
1484
+ const fetched = await runGit(gitDeps, ["fetch", "origin", scopeBaseBranch]);
1485
+ if (fetched.exitCode !== 0) {
1486
+ deps.errorLog(`git fetch origin ${scopeBaseBranch} failed in ${deps.cwd}. Run setup-epic from a clone ` +
1487
+ `of ${access.repoName} with an 'origin' remote. No run was created and no branch was changed.`);
1488
+ return 1;
1489
+ }
1490
+ if (!(await ensureCommitResolvableLocally(gitDeps, cutCommitSha))) {
1491
+ deps.errorLog(`The canonical indexed commit ${cutCommitSha} could not be resolved locally even after ` +
1492
+ `fetching it from origin. Fetch it manually, or re-parse ${access.repoName}. No run was ` +
1493
+ `created and no branch was changed.`);
1494
+ return 1;
1495
+ }
1496
+ const remoteHead = await readRemoteBranchHead(gitDeps, effectiveFeatureBranch);
1497
+ if (!remoteHead.ok) {
1498
+ deps.errorLog(`${remoteHead.error} No run was created and no branch was changed.`);
1499
+ return 1;
1500
+ }
1501
+ if (remoteHead.sha !== null && remoteHead.sha !== cutCommitSha) {
1502
+ deps.errorLog(`origin/${effectiveFeatureBranch} already exists at ${remoteHead.sha}, which is not the ` +
1503
+ `canonical indexed commit ${cutCommitSha}. setup-epic never force-updates an existing ` +
1504
+ `branch: delete it, or finish the previous run first. No run was created and no branch was changed.`);
1505
+ return 1;
1506
+ }
1507
+ say(remoteHead.sha === null
1508
+ ? `Branch: origin/${effectiveFeatureBranch} is absent; it will be cut at ${cutCommitSha}`
1509
+ : `Branch: origin/${effectiveFeatureBranch} already sits at the cut commit ${cutCommitSha}`);
1510
+ }
1262
1511
  const result = {
1263
1512
  epic_run_id: existingRunId ?? "",
1264
1513
  epic_key: opts.epicKey,
@@ -1274,8 +1523,10 @@ export async function runSetupEpicCli(argv, overrides = {}) {
1274
1523
  policy_applied: "absent",
1275
1524
  warnings,
1276
1525
  };
1277
- if (featureBranch !== undefined)
1278
- result.feature_branch = featureBranch;
1526
+ // BAPI-850: the EFFECTIVE feature branch (flag or policy file), so a
1527
+ // policy-file-declared feature run reports the same key a flag run does.
1528
+ if (effectiveFeatureBranch !== undefined)
1529
+ result.feature_branch = effectiveFeatureBranch;
1279
1530
  // --- Step 1: create (only when there is no live run) ---------------------
1280
1531
  if (existingRunId) {
1281
1532
  say(`Run: reusing ${existingRunId} (status: ${existingStatus})`);
@@ -1366,6 +1617,40 @@ export async function runSetupEpicCli(argv, overrides = {}) {
1366
1617
  return 1;
1367
1618
  }
1368
1619
  }
1620
+ // --- The exact cut (BAPI-850) --------------------------------------------
1621
+ // AFTER the run exists and BEFORE the plan is stored/approved: the scope's run
1622
+ // association is immutable server-side, so the cut is driven with this run's
1623
+ // id and the scope is minted owned by it. Everything from `cut/begin` to the
1624
+ // hold's release is the SHARED protocol `conduct-epic init` drives — local git
1625
+ // only, an exact-SHA create-only push, never the GitHub App, never a force
1626
+ // update. A failure leaves the run non-active (never discoverable by the
1627
+ // reconciler) and re-running setup-epic reuses it and re-drives the cut.
1628
+ let cutScopeId = null;
1629
+ if (needsCut && cutCommitSha !== null && scopeBaseBranch !== null && cutRunCommand !== undefined) {
1630
+ const cut = await performExactIndexScopeCut(cutProtocolDeps(deps, cutRunCommand), access, {
1631
+ featureBranch: effectiveFeatureBranch,
1632
+ baseBranch: scopeBaseBranch,
1633
+ candidateCommitSha: cutCommitSha,
1634
+ epicRunId: result.epic_run_id,
1635
+ });
1636
+ if (!cut.ok) {
1637
+ for (const line of cut.failures)
1638
+ deps.errorLog(line);
1639
+ if (cut.kind === "existing_ref_mismatch" || cut.kind === "confirm_failed") {
1640
+ deps.errorLog(`Expected origin/${effectiveFeatureBranch} at the canonical indexed commit ` +
1641
+ `${cut.expectedSha ?? cutCommitSha}` +
1642
+ (cut.observedSha ? `, found ${cut.observedSha}.` : ".") +
1643
+ ` setup-epic never force-pushes: correct the local/origin branch, then re-run setup-epic.`);
1644
+ }
1645
+ deps.errorLog(`Epic run ${result.epic_run_id} exists but was NOT approved and will not dispatch; ` +
1646
+ `re-running setup-epic reuses it and re-drives the cut.`);
1647
+ return emitRefusal(deps, opts, result);
1648
+ }
1649
+ cutScopeId = cut.lease.scope_id;
1650
+ say(cut.branchCreated
1651
+ ? `Branch: cut origin/${effectiveFeatureBranch} at ${cut.lease.cut_commit_sha} with local git (scope ${cutScopeId})`
1652
+ : `Branch: origin/${effectiveFeatureBranch} confirmed at ${cut.lease.cut_commit_sha} (scope ${cutScopeId}, cut already recorded)`);
1653
+ }
1369
1654
  // --- Step 2/3: store the plan -------------------------------------------
1370
1655
  try {
1371
1656
  const stored = await storeEpicPlan(access, {
@@ -1390,23 +1675,24 @@ export async function runSetupEpicCli(argv, overrides = {}) {
1390
1675
  deps.errorLog(`Failed to store the plan: ${errorDetail(err)}`);
1391
1676
  return 1;
1392
1677
  }
1393
- // --- Step 4: approve (also provisions the feature branch server-side) -----
1394
- // For a feature-branch run the backend creates/validates the branch on origin
1395
- // as an approval prerequisite. Provisioning runs and completes BEFORE the
1396
- // approval (ticket-seeding + CAS activation) step server-side, so only a
1397
- // failure the backend tags with the structured FEATURE_BRANCH_PROVISIONING
1398
- // error_code is actually a branch/credentials problem key the distinct
1399
- // message off that code, not merely off whether --feature-branch was passed,
1400
- // so an unrelated approval failure (e.g. ticket-seeding, a masked 500) is not
1401
- // misattributed to provisioning.
1402
- if (featureBranch !== undefined) {
1403
- say(`Branch: creating or validating ${featureBranch} on origin…`);
1678
+ // --- Step 4: approve (also validates the feature branch server-side) ------
1679
+ // For a feature-branch run the backend READ-validates the branch on origin
1680
+ // against the scope's recorded cut as an approval prerequisite (BAPI-850: the
1681
+ // local cut above created it; the App never creates refs). Validation runs
1682
+ // and completes BEFORE the approval (ticket-seeding + CAS activation) step
1683
+ // server-side, so only a failure the backend tags with the structured
1684
+ // FEATURE_BRANCH_PROVISIONING error_code is actually a branch/scope/credentials
1685
+ // problem key the distinct message off that code, not merely off whether a
1686
+ // feature branch is set, so an unrelated approval failure (e.g.
1687
+ // ticket-seeding, a masked 500) is not misattributed to provisioning.
1688
+ if (effectiveFeatureBranch !== undefined) {
1689
+ say(`Branch: validating origin/${effectiveFeatureBranch} server-side (App read) and preparing the index scope…`);
1404
1690
  }
1405
1691
  const approval = await approveEpicPlan(access, { epicKey: opts.epicKey, planVersion: plan.plan_version }, deps.fetch).catch((err) => {
1406
- if (featureBranch !== undefined &&
1692
+ if (effectiveFeatureBranch !== undefined &&
1407
1693
  err instanceof ConductorBridgeApiError &&
1408
1694
  err.errorCode === "FEATURE_BRANCH_PROVISIONING") {
1409
- deps.errorLog(`Failed to provision the feature branch '${featureBranch}' — child-ticket ` +
1695
+ deps.errorLog(`Failed to provision the feature branch '${effectiveFeatureBranch}' — child-ticket ` +
1410
1696
  `dispatch has NOT started. Correct repository access or the branch ` +
1411
1697
  `configuration, then re-run setup-epic.\nDetail: ${errorDetail(err)}`);
1412
1698
  }
@@ -1426,12 +1712,14 @@ export async function runSetupEpicCli(argv, overrides = {}) {
1426
1712
  if (prov) {
1427
1713
  result.feature_branch_provisioning = prov;
1428
1714
  if (prov.status === "created") {
1715
+ // Legacy servers only: a BAPI-850 server never creates a ref.
1429
1716
  say(`Branch: ready on origin — created '${prov.feature_branch}' from ` +
1430
1717
  `'${prov.source_branch}' at ${prov.source_sha}`);
1431
1718
  }
1432
1719
  else {
1433
- say(`Branch: '${prov.feature_branch}' already exists validated, unchanged ` +
1434
- `(the remote ref was not moved or reset); head ${prov.remote_head_sha}`);
1720
+ say(`Branch: '${prov.feature_branch}' validated on origin at head ` +
1721
+ `${prov.remote_head_sha} (cut ${prov.source_sha} from '${prov.source_branch}'; ` +
1722
+ `the remote ref was not moved or reset)`);
1435
1723
  }
1436
1724
  }
1437
1725
  }
@@ -1452,6 +1740,93 @@ export async function runSetupEpicCli(argv, overrides = {}) {
1452
1740
  result.warnings.push(`Server plan hash differs from the local hash (the server re-hashes after ` +
1453
1741
  `applying file-overlap serialization). The server hash is authoritative.`);
1454
1742
  }
1743
+ // --- Bounded scope lifecycle feedback (BAPI-850) --------------------------
1744
+ // Only after a feature-bearing approval succeeded (or an idempotent approval
1745
+ // confirmed an already-active run). The scope id comes from THIS invocation's
1746
+ // cut, or — for a reused run whose cut an earlier invocation recorded — from
1747
+ // the run's persisted scope association. Transition prose goes to `say`
1748
+ // (stderr under --json) so stdout stays the single result document. The poll
1749
+ // reuses the pilot's interval, cap, and status vocabulary; `ready` is accepted
1750
+ // only when the server proves indexed == cut.
1751
+ let exitCode = 0;
1752
+ if (scopeBearing) {
1753
+ const scopeId = cutScopeId ?? (await resolveScopeIdForRun(access, deps.fetch, result.epic_run_id));
1754
+ if (scopeId === null) {
1755
+ result.index_scope = {
1756
+ scope_id: null,
1757
+ cut_commit_sha: cutCommitSha,
1758
+ lifecycle_state: null,
1759
+ ready: false,
1760
+ disposition: "not_polled",
1761
+ };
1762
+ say(`Scope: no index scope is associated with run ${result.epic_run_id}; ` +
1763
+ `the reconciler will not dispatch until one is ready. Re-run setup-epic.`);
1764
+ exitCode = 1;
1765
+ }
1766
+ else {
1767
+ say(`Scope: waiting for index scope readiness (bounded)…`);
1768
+ const verdict = await pollIndexScopeLifecycle(cutProtocolDeps(deps, cutRunCommand ?? deps.runCommand ?? createExecFileRunCommand()), access, scopeId, { onTransition: (state, status) => say(describeScopeTransition(state, status)) });
1769
+ if (verdict.kind === "ready") {
1770
+ result.index_scope = {
1771
+ scope_id: scopeId,
1772
+ cut_commit_sha: verdict.status.cut_commit_sha,
1773
+ lifecycle_state: verdict.status.lifecycle_state,
1774
+ ready: true,
1775
+ disposition: "ready",
1776
+ };
1777
+ say(`Scope: ready — the reconciler may dispatch against the epic branch index.`);
1778
+ }
1779
+ else if (verdict.kind === "ready_mismatch") {
1780
+ result.index_scope = {
1781
+ scope_id: scopeId,
1782
+ cut_commit_sha: verdict.status.cut_commit_sha,
1783
+ lifecycle_state: verdict.status.lifecycle_state,
1784
+ ready: false,
1785
+ disposition: "failed",
1786
+ failure_reason: "ready_watermark_mismatch",
1787
+ recovery_command: SETUP_EPIC_SCOPE_RECOVERY_COMMAND,
1788
+ };
1789
+ deps.errorLog(`Index scope ${scopeId} reports ready but its indexed commit ` +
1790
+ `(${verdict.status.indexed_commit_sha ?? "none"}) is not the cut commit ` +
1791
+ `(${verdict.status.cut_commit_sha ?? "none"}). Treating the scope as NOT ready; ` +
1792
+ `ticket dispatch remains blocked by the reconciler's freshness hold. ` +
1793
+ `Recovery: ${SETUP_EPIC_SCOPE_RECOVERY_COMMAND}.`);
1794
+ exitCode = 1;
1795
+ }
1796
+ else if (verdict.kind === "failed") {
1797
+ result.index_scope = {
1798
+ scope_id: scopeId,
1799
+ cut_commit_sha: verdict.status.cut_commit_sha,
1800
+ lifecycle_state: verdict.status.lifecycle_state,
1801
+ ready: false,
1802
+ disposition: "failed",
1803
+ failure_reason: verdict.reason,
1804
+ recovery_command: SETUP_EPIC_SCOPE_RECOVERY_COMMAND,
1805
+ };
1806
+ deps.errorLog(`Index scope ${scopeId} FAILED (reason: ${verdict.reason}) — this is a recorded ` +
1807
+ `failure, not an in-progress state. Ticket dispatch remains blocked: the reconciler ` +
1808
+ `will not dispatch against a failed scope.`);
1809
+ deps.errorLog(scopeFailureGuidance(verdict.reason, effectiveFeatureBranch ?? ""));
1810
+ exitCode = 1;
1811
+ }
1812
+ else {
1813
+ result.index_scope = {
1814
+ scope_id: scopeId,
1815
+ cut_commit_sha: verdict.lastStatus?.cut_commit_sha ?? cutCommitSha,
1816
+ lifecycle_state: verdict.lastStatus?.lifecycle_state ?? null,
1817
+ ready: false,
1818
+ disposition: "timeout",
1819
+ };
1820
+ deps.errorLog(`Index scope ${scopeId} did not become ready within the bounded wait ` +
1821
+ `(last observed state: ${verdict.lastState}). The run ${result.epic_run_id} is ` +
1822
+ `created and scope preparation continues asynchronously on the server; the ` +
1823
+ `reconciler will NOT dispatch tickets until the scope becomes ready. Check ` +
1824
+ `GET /jira/index-scope/status?repo_name=${access.repoName}&scope_id=${scopeId}, ` +
1825
+ `or re-run setup-epic to wait again.`);
1826
+ exitCode = 1;
1827
+ }
1828
+ }
1829
+ }
1455
1830
  if (opts.json) {
1456
1831
  deps.log(JSON.stringify(result, null, 2));
1457
1832
  }
@@ -1468,7 +1843,7 @@ export async function runSetupEpicCli(argv, overrides = {}) {
1468
1843
  say(`Epic run ${result.epic_run_id} is ${result.status ?? "unknown"}.`);
1469
1844
  say("The server-side reconciler will pick it up within ~30s.");
1470
1845
  say("To execute claimed jobs on this machine, run:");
1471
- say(` npx -y @bridge_gpt/mcp-server executor --repo ${access.repoName}`);
1846
+ say(` npx -y ${MCP_PACKAGE_NAME} executor --repo ${access.repoName}`);
1472
1847
  }
1473
- return 0;
1848
+ return exitCode;
1474
1849
  }