@bridge_gpt/mcp-server 0.2.41 → 0.2.43

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 (88) hide show
  1. package/README.md +330 -191
  2. package/build/agent-capabilities/cli.js +2 -1
  3. package/build/agent-launchers/claude-executor-adapter.js +17 -4
  4. package/build/agents.generated.js +2 -2
  5. package/build/claude-review-workflow.js +510 -45
  6. package/build/claude-user-config-doctor.js +42 -11
  7. package/build/cli-release.js +2 -1
  8. package/build/commands.generated.js +6 -5
  9. package/build/conduct-epic/bridge-client.js +354 -113
  10. package/build/conduct-epic/checkpoint-store.js +17 -0
  11. package/build/conduct-epic/cli.js +947 -99
  12. package/build/conduct-epic/cut-protocol.js +327 -0
  13. package/build/conduct-epic/spawn.js +14 -2
  14. package/build/conductor/bridge-api-client.js +148 -1
  15. package/build/conductor/cli.js +109 -1
  16. package/build/conductor/doctor.js +101 -16
  17. package/build/conductor/epic-reconcile.js +72 -19
  18. package/build/conductor/epic-runtime.js +15 -3
  19. package/build/conductor/errors.js +47 -0
  20. package/build/conductor/git-hooks.js +205 -11
  21. package/build/conductor/install-doctor.js +230 -1
  22. package/build/conductor/local-merge.js +130 -28
  23. package/build/conductor/recovery-cli.js +313 -0
  24. package/build/conductor/recovery-operations.js +219 -0
  25. package/build/conductor/tools.js +32 -3
  26. package/build/conductor/worker-ledger-cli.js +27 -1
  27. package/build/conductor-bin.js +20 -16
  28. package/build/credentials-cli.js +3 -2
  29. package/build/docs.generated.js +2 -1
  30. package/build/doctor.js +120 -44
  31. package/build/drive-epic.js +375 -0
  32. package/build/executor/cli.js +48 -1
  33. package/build/executor/env.js +21 -0
  34. package/build/executor/http-client.js +71 -3
  35. package/build/executor/index-scope.js +39 -0
  36. package/build/executor/job-errors.js +9 -0
  37. package/build/executor/job-log-registry.js +69 -0
  38. package/build/executor/job-runner.js +198 -29
  39. package/build/executor/live-worker-registry.js +83 -0
  40. package/build/executor/observation.js +259 -6
  41. package/build/executor/platform.js +147 -3
  42. package/build/executor/process.js +58 -14
  43. package/build/executor/runner.js +454 -48
  44. package/build/executor/test-clock.js +3 -2
  45. package/build/executor/worker-finalization.js +233 -56
  46. package/build/executor/worktree.js +8 -1
  47. package/build/index-scope-contract.js +96 -0
  48. package/build/index.js +2277 -270
  49. package/build/init.js +83 -22
  50. package/build/install-bridge-conductor.js +323 -14
  51. package/build/install-bridge.js +225 -47
  52. package/build/install-doctor.js +23 -9
  53. package/build/install-reexec.js +2 -1
  54. package/build/launcher-config-inspection.js +83 -22
  55. package/build/mcp-host-config.js +331 -67
  56. package/build/mcp-host-targets.js +45 -21
  57. package/build/mcp-identity.js +92 -0
  58. package/build/mcp-install-state.js +94 -1
  59. package/build/mcp-invoke.js +2 -1
  60. package/build/mcp-provisioning.js +45 -12
  61. package/build/mcp-registration-doctor.js +35 -13
  62. package/build/mcp-server-invocation.js +4 -2
  63. package/build/merge-pull-request.js +208 -9
  64. package/build/pipelines.generated.js +305 -15
  65. package/build/plane/cli.js +73 -7
  66. package/build/plane/defaults.js +18 -5
  67. package/build/plane/manifest.js +90 -0
  68. package/build/plane/preflight.js +100 -10
  69. package/build/plane/shutdown.js +71 -3
  70. package/build/plane/test-fakes.js +9 -1
  71. package/build/readme.generated.js +1 -1
  72. package/build/regression-check.js +3 -2
  73. package/build/review-tickets.js +8 -7
  74. package/build/run-unit-tests-launcher.js +149 -6
  75. package/build/schedule-run.js +3 -2
  76. package/build/setup-epic.js +531 -82
  77. package/build/sfcc/tool-wrapper.js +15 -0
  78. package/build/start-tickets-prereqs.js +11 -6
  79. package/build/start-tickets.js +91 -85
  80. package/build/update-check.js +3 -2
  81. package/build/upgrade-advice.js +2 -1
  82. package/build/upgrade-cli.js +50 -18
  83. package/build/version.generated.js +2 -1
  84. package/build/worktree-core.js +31 -17
  85. package/docs/CONDUCTOR.md +22 -0
  86. package/docs/install/mcp-tool-integrations.md +19 -3
  87. package/package.json +2 -2
  88. package/pipelines/greenfield-setup.json +286 -0
@@ -30,55 +30,68 @@
30
30
  * file-overlap serialization, so a divergence is legitimate, not corruption.
31
31
  * 5. Store, then approve.
32
32
  *
33
- * Read-only against the local filesystem: it reads the plan sidecar and the
34
- * optional policy file, and writes nothing.
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
+ *
58
+ * Reads the plan sidecar and the optional policy file from the local
59
+ * filesystem. As of BAPI-872 it can also WRITE the local filesystem in one
60
+ * narrow case: after a successful create/reuse, if a live `plane up` manifest
61
+ * exists for this exact repository root, setup-epic binds the resolved
62
+ * `epic_run_id` onto it (`.bridge/plane/plane.json`) so a later `plane down`
63
+ * can stop that run automatically instead of guessing from repository-wide
64
+ * active-run state. Every other workflow (remote, no local plane) is
65
+ * unaffected and performs no such write.
35
66
  */
36
67
  import { readFile as fsReadFile, stat as fsStat } from "node:fs/promises";
37
68
  import os from "node:os";
38
69
  import readline from "node:readline";
39
70
  import { approveEpicPlan, createEpicRunWithDisposition, fetchEpicRunState, replaceEpicRunPolicy, resolveConductorBridgeApiAccess, storeEpicPlan, validateEpicPlan, ConductorBridgeApiError, } from "./conductor/bridge-api-client.js";
40
71
  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
72
+ // Reused from the conduct-epic client on purpose: adding a second wrapper under
43
73
  // `conductor/` would give the v2 path its own copy to drift from.
44
- import { getIndexBranch } from "./conduct-epic/bridge-client.js";
74
+ //
75
+ // BAPI-850 extends that reuse to the canonical parse read, the repository base
76
+ // branch, the per-scope status/lifecycle reads, and the exact-cut protocol: the
77
+ // typed clients the pilot already has are the v2 path's clients too, so neither
78
+ // conductor can drift on a field name or an endpoint shape. No MCP tool, no
79
+ // input-schema field, and no description changes.
80
+ import { getConfigFieldBaseBranch, getIndexScopeLifecycle, getParseStatus, } from "./conduct-epic/bridge-client.js";
81
+ import { createExecFileRunCommand, ensureCommitResolvableLocally, normalizeCommitSha, performExactIndexScopeCut, pollIndexScopeLifecycle, readRemoteBranchHead, runGit, SCOPE_LIFECYCLE_LABELS, } from "./conduct-epic/cut-protocol.js";
45
82
  import { hashPlan } from "./conductor/plan.js";
83
+ import { MCP_PACKAGE_NAME } from "./mcp-identity.js";
84
+ // BAPI-872: local plane-manifest binding, so `plane down` can later stop the
85
+ // run this setup resolved WITHOUT guessing from repository-wide active-run
86
+ // state. See `planeBinding` on `SetupEpicDeps` below.
87
+ import { createPlaneFsDeps, createPlaneProcessDeps } from "./plane/defaults.js";
88
+ import { bindPlaneManifestEpicRun, manifestHasLiveProcess, readPlaneManifest, } from "./plane/manifest.js";
46
89
  /** Accepted `policy_json.review_policy.source` values (the `ReviewPolicy` surface). */
47
90
  export const SETUP_EPIC_REVIEW_POLICY_SOURCES = [
48
91
  "verdict_protocol",
49
92
  "native_review_decision",
50
93
  "none",
51
94
  ];
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
95
  /** Echoed single-line prompt on stderr (mirrors connect-github's helper). */
83
96
  function defaultPromptLine(promptText) {
84
97
  return new Promise((resolve) => {
@@ -111,6 +124,12 @@ export function createDefaultSetupEpicDeps() {
111
124
  errorLog: (m) => console.error(m),
112
125
  isTTY: Boolean(process.stdin.isTTY),
113
126
  promptLine: defaultPromptLine,
127
+ runCommand: createExecFileRunCommand(),
128
+ planeBinding: {
129
+ readManifest: (repoRoot) => readPlaneManifest(repoRoot, createPlaneFsDeps()),
130
+ isPlaneProcessAlive: (pid) => createPlaneProcessDeps().isAlive(pid),
131
+ bind: (repoRoot, planeId, epicRunId) => bindPlaneManifestEpicRun(repoRoot, planeId, epicRunId, createPlaneFsDeps()),
132
+ },
114
133
  };
115
134
  }
116
135
  /** User-facing usage text. */
@@ -129,10 +148,14 @@ export function getSetupEpicUsage() {
129
148
  " --repo <name> Repo name (default: BAPI_REPO_NAME or .bridge/config)",
130
149
  " --plan-version <n> Assert the sidecar's plan_version equals <n>",
131
150
  " --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.",
151
+ " setup-epic cuts the branch on origin with your LOCAL git",
152
+ " at exactly the repository's canonical indexed commit",
153
+ " (the repository must be parsed first), the server",
154
+ " validates it, and every child-ticket PR targets it.",
155
+ " setup-epic then waits (bounded) for the run's index",
156
+ " scope to become ready. Omit (the default) to continue",
157
+ " on the repository base branch. Interactive runs are",
158
+ " offered a proposal.",
136
159
  " --review-policy <src> PER-RUN review policy source, one of:",
137
160
  ` ${SETUP_EPIC_REVIEW_POLICY_SOURCES.join(", ")}.`,
138
161
  " Composed into policy_json.review_policy on create.",
@@ -152,6 +175,12 @@ export function getSetupEpicUsage() {
152
175
  " post-create PATCH is needed. Contradicts nothing",
153
176
  " silently: if the file and --feature-branch or",
154
177
  " --review-policy disagree, setup-epic errors naming both.",
178
+ " policy_json.job_timeouts values must be below the job",
179
+ " type's gate ceiling (implement / spec_review / resume",
180
+ " 10799; remediate / ci_fix 3599; rebase / smoke 2699;",
181
+ " merge 1799); the server rejects a higher value with a",
182
+ " 422 naming the offending field. Full table:",
183
+ " docs/claude/epic-conductor-v2-operator-runbook.md §4.",
155
184
  " --replace-policy Authorize replacing a LIVE run's stored policy with the",
156
185
  " policy file, as a complete replacement. Without it, a",
157
186
  " divergent policy on a reused run is refused with a",
@@ -170,7 +199,7 @@ export function getSetupEpicUsage() {
170
199
  "",
171
200
  "After setup, the server-side reconciler picks the run up within ~30s.",
172
201
  "To execute claimed jobs on this machine, run:",
173
- " npx -y @bridge_gpt/mcp-server executor --repo <name>",
202
+ ` npx -y ${MCP_PACKAGE_NAME} executor --repo <name>`,
174
203
  ].join("\n");
175
204
  }
176
205
  function takeValue(argv, i, flag) {
@@ -897,6 +926,151 @@ function emitRefusal(deps, opts, result) {
897
926
  deps.log(JSON.stringify(result, null, 2));
898
927
  return 1;
899
928
  }
929
+ // ---------------------------------------------------------------------------
930
+ // Feature-branch start sequence helpers (BAPI-850)
931
+ // ---------------------------------------------------------------------------
932
+ /**
933
+ * The one recovery action for a scope that the server recorded as `failed`.
934
+ *
935
+ * Approval is idempotent and the server re-drives a `failed` scope's seed and
936
+ * verification when it is re-approved, so re-running the SAME `setup-epic`
937
+ * invocation is the canonical fix. The `canonical_index_advanced` category is
938
+ * the exception the pilot also calls out: the cut itself must be re-driven at
939
+ * the newer commit.
940
+ */
941
+ 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)";
942
+ /** The lead sentence of every fail-closed canonical-parse refusal (BAPI-850). */
943
+ export const SETUP_EPIC_PARSE_FIRST_MESSAGE = "parse the repository first";
944
+ /**
945
+ * Resolve the EFFECTIVE feature branch after policy composition.
946
+ *
947
+ * `--feature-branch` wins when given. Otherwise a valid `policy_json.base_branch`
948
+ * supplied by `--policy-file` names the branch — the file ALREADY answered the
949
+ * question, and the run will be created with that branch whether or not this
950
+ * CLI noticed. A malformed or unknown policy field (including the camelCase
951
+ * `baseBranch` slip) is deliberately NOT interpreted here: the server's strict
952
+ * `RunPolicy` validation owns rejecting it, so this returns `undefined` and the
953
+ * request proceeds to that 422 unchanged.
954
+ */
955
+ export function resolveEffectiveFeatureBranch(featureBranch, requestedPolicy) {
956
+ if (featureBranch !== undefined)
957
+ return featureBranch;
958
+ const raw = requestedPolicy?.["base_branch"];
959
+ if (typeof raw !== "string")
960
+ return undefined;
961
+ const trimmed = raw.trim();
962
+ if (trimmed === "" || validateBranchName(trimmed) !== null)
963
+ return undefined;
964
+ return trimmed;
965
+ }
966
+ /**
967
+ * Read canonical parse truth and require a `succeeded` parse with a full
968
+ * indexed commit — the commit `origin/<feature>` will be cut at.
969
+ *
970
+ * Fails CLOSED (ratified R-2). The first line of every refusal is the exact
971
+ * operator instruction; the closing lines state what did NOT happen. An
972
+ * UNREADABLE status is kept distinct from an unsuccessful one — "could not read"
973
+ * is not evidence the repository is unindexed, and telling that operator to
974
+ * re-index would send them to re-parse a repository that may be fine.
975
+ */
976
+ export async function readCanonicalParseTruth(access, fetchImpl) {
977
+ const notCreated = [
978
+ "No run was created and no branch was changed.",
979
+ ];
980
+ const parse = await getParseStatus(access, fetchImpl);
981
+ if (!parse.ok) {
982
+ return {
983
+ ok: false,
984
+ lines: [
985
+ `The canonical parse status for ${access.repoName} could not be read: ${parse.error}. ` +
986
+ `Retry; if the repository has never been parsed, ${SETUP_EPIC_PARSE_FIRST_MESSAGE}.`,
987
+ ...notCreated,
988
+ ],
989
+ };
990
+ }
991
+ if (parse.value.status !== "succeeded") {
992
+ return {
993
+ ok: false,
994
+ lines: [
995
+ `${SETUP_EPIC_PARSE_FIRST_MESSAGE}: the canonical index for ${access.repoName} has no ` +
996
+ `successful parse (status: ${String(parse.value.status)}), so there is no commit to ` +
997
+ `cut the feature branch at.`,
998
+ ...notCreated,
999
+ ],
1000
+ };
1001
+ }
1002
+ const indexed = normalizeCommitSha(parse.value.indexed_commit_sha);
1003
+ if (indexed === null) {
1004
+ return {
1005
+ ok: false,
1006
+ lines: [
1007
+ `${SETUP_EPIC_PARSE_FIRST_MESSAGE}: the canonical index for ${access.repoName} published ` +
1008
+ `no commit for its last successful parse, so there is no commit to cut the feature ` +
1009
+ `branch at.`,
1010
+ ...notCreated,
1011
+ ],
1012
+ };
1013
+ }
1014
+ return { ok: true, cutCommitSha: indexed };
1015
+ }
1016
+ /** The strict subset of `SetupEpicDeps` the shared cut protocol consumes. */
1017
+ function cutProtocolDeps(deps, runCommand) {
1018
+ return {
1019
+ runCommand,
1020
+ cwd: deps.cwd,
1021
+ fetchImpl: deps.fetch,
1022
+ errorLog: deps.errorLog,
1023
+ sleep: deps.sleep,
1024
+ };
1025
+ }
1026
+ /**
1027
+ * Resolve the run's scope id for a REUSED, already-activated run, whose cut was
1028
+ * recorded by an earlier invocation and must not be re-driven.
1029
+ *
1030
+ * Reads the repository's lifecycle listing (an existing read) and matches on the
1031
+ * server-recorded `epic_run_id`. Returns `null` when no scope is associated —
1032
+ * the server's approval reports that case itself.
1033
+ */
1034
+ async function resolveScopeIdForRun(access, fetchImpl, epicRunId) {
1035
+ const listing = await getIndexScopeLifecycle(access, fetchImpl);
1036
+ if (!listing.ok)
1037
+ return null;
1038
+ const match = listing.value.scopes.find((scope) => scope.epic_run_id === epicRunId);
1039
+ return match?.scope_id ?? null;
1040
+ }
1041
+ /**
1042
+ * Operator-facing guidance for a bounded scope failure category.
1043
+ *
1044
+ * BAPI-872: abandonment guidance names the conductor CLI verb — never a raw
1045
+ * `PATCH` recipe an operator would have to hand-roll. `epicRunId` is the
1046
+ * concrete, already-resolved run ID at this call site (the failure is scoped to
1047
+ * ONE run), so it is always named explicitly rather than left as a placeholder.
1048
+ */
1049
+ function scopeFailureGuidance(reason, featureBranch, epicRunId) {
1050
+ if (reason === "canonical_index_advanced") {
1051
+ return (`The canonical index advanced before the scope could be seeded. The cut must be ` +
1052
+ `re-driven at the newer commit: delete origin/${featureBranch}, abandon this run ` +
1053
+ `(conductor abandon-run --epic-run-id ${epicRunId}), and re-run setup-epic.`);
1054
+ }
1055
+ return `Recovery: ${SETUP_EPIC_SCOPE_RECOVERY_COMMAND}.`;
1056
+ }
1057
+ /**
1058
+ * Report one newly observed lifecycle transition, once, in order.
1059
+ *
1060
+ * Prior lines stay on screen as confirmation — no spinner rewrite, no
1061
+ * percentages. Branch, SHA, and scope identifiers are secondary technical
1062
+ * detail: the label leads, the scope id never does, and no credential appears.
1063
+ */
1064
+ function describeScopeTransition(state, status) {
1065
+ const label = SCOPE_LIFECYCLE_LABELS[state] ?? state;
1066
+ if (state === "ready") {
1067
+ return `Scope: ${label} — indexed at ${status.indexed_commit_sha ?? "unknown"} (scope ${status.scope_id})`;
1068
+ }
1069
+ if (state === "failed") {
1070
+ return `Scope: ${label} — ${status.last_error ?? "unknown"} (scope ${status.scope_id})`;
1071
+ }
1072
+ return `Scope: ${label}…`;
1073
+ }
900
1074
  export async function runSetupEpicCli(argv, overrides = {}) {
901
1075
  const deps = { ...createDefaultSetupEpicDeps(), ...overrides };
902
1076
  const parsed = parseSetupEpicArgs(argv);
@@ -1019,21 +1193,6 @@ export async function runSetupEpicCli(argv, overrides = {}) {
1019
1193
  say(`Local hash: ${localHash}`);
1020
1194
  for (const w of warnings)
1021
1195
  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
1196
  // --- Feature branch selection (BAPI-655) --------------------------------
1038
1197
  // Resolved AFTER local/access context is known but BEFORE any run-state read
1039
1198
  // or mutating request, so malformed interactive input fails before network
@@ -1041,7 +1200,8 @@ export async function runSetupEpicCli(argv, overrides = {}) {
1041
1200
  const promptedInteractively = opts.featureBranch === undefined && !policyDeclaresBranch && deps.isTTY && !opts.json;
1042
1201
  const featureBranch = await resolveFeatureBranchSelection(opts, access.repoName, deps, policyDeclaresBranch);
1043
1202
  if (featureBranch !== undefined) {
1044
- say(`Feature: ${featureBranch} (create from repository base branch on origin)`);
1203
+ say(`Feature: ${featureBranch} (cut on origin with local git at the canonical indexed ` +
1204
+ `commit; validated server-side at approval)`);
1045
1205
  }
1046
1206
  else if (promptedInteractively) {
1047
1207
  // Ordinary, non-warning notice — only when an interactive operator declined.
@@ -1064,6 +1224,15 @@ export async function runSetupEpicCli(argv, overrides = {}) {
1064
1224
  requestedPolicy = composed.policy;
1065
1225
  say(`Policy: ${opts.policyFile} (complete run policy, applied at creation)`);
1066
1226
  }
1227
+ // --- Effective feature branch (BAPI-850) ----------------------------------
1228
+ // Derived AFTER policy composition so a branch the policy file declares under
1229
+ // `base_branch` is honored exactly as `--feature-branch` would be. A malformed
1230
+ // or unknown policy field is left for the server's strict validation.
1231
+ const effectiveFeatureBranch = resolveEffectiveFeatureBranch(featureBranch, requestedPolicy);
1232
+ if (effectiveFeatureBranch !== undefined && featureBranch === undefined) {
1233
+ say(`Feature: ${effectiveFeatureBranch} (from the policy file; cut on origin with local ` +
1234
+ `git at the canonical indexed commit; validated server-side at approval)`);
1235
+ }
1067
1236
  // --- Step 0: pre-check ---------------------------------------------------
1068
1237
  // Never create on an ambiguous read. A wrong answer here mints a duplicate run.
1069
1238
  let existingRunId = null;
@@ -1106,7 +1275,7 @@ export async function runSetupEpicCli(argv, overrides = {}) {
1106
1275
  else if (err instanceof ConductorBridgeApiError && err.status === 409) {
1107
1276
  deps.errorLog(`Epic ${opts.epicKey} has MULTIPLE active runs — it is wedged, and every ` +
1108
1277
  `plan call will keep failing. Abandon the duplicate before retrying:\n` +
1109
- ` PATCH /jira/epic-runs/runs/<epic_run_id> {"status": "abandoned"}\n` +
1278
+ ` conductor abandon-run --epic-run-id <duplicate_epic_run_id>\n` +
1110
1279
  `Detail: ${errorDetail(err)}`);
1111
1280
  return 1;
1112
1281
  }
@@ -1192,7 +1361,7 @@ export async function runSetupEpicCli(argv, overrides = {}) {
1192
1361
  `validated because the epic itself is wedged, and every plan call will ` +
1193
1362
  `keep failing. This is NOT a problem with your plan. Abandon the ` +
1194
1363
  `duplicate before retrying:\n` +
1195
- ` PATCH /jira/epic-runs/runs/<epic_run_id> {"status": "abandoned"}\n` +
1364
+ ` conductor abandon-run --epic-run-id <duplicate_epic_run_id>\n` +
1196
1365
  `No run was created and no automation-start charge occurred.\n` +
1197
1366
  `Detail: ${errorDetail(err)}`);
1198
1367
  return 1;
@@ -1206,8 +1375,64 @@ export async function runSetupEpicCli(argv, overrides = {}) {
1206
1375
  if (validated) {
1207
1376
  say("Validation: server validation: passed");
1208
1377
  say(` plan hash ${validated.planHash}`);
1209
- say(` serialization ${validated.serializationEnabled ? "enabled" : "disabled"}, ` +
1210
- `${validated.insertedEdges} edge(s) inserted`);
1378
+ // BAPI-848 three distinct outcomes, never collapsed into success language.
1379
+ // "0 edges inserted" used to read as proof the siblings could not conflict.
1380
+ // It never was: serialization only sees paths BOTH nodes declared, and the
1381
+ // repo's worst conflict hotspot was one nobody declared.
1382
+ if (!validated.serializationEnabled) {
1383
+ say(" serialization disabled — no ordering was derived from file overlap");
1384
+ }
1385
+ else if (validated.insertedEdges === 0) {
1386
+ say(" serialization enabled — no declared file overlaps produced edges");
1387
+ }
1388
+ else {
1389
+ say(` serialization enabled — ${validated.insertedEdges} edge(s) inserted from ` +
1390
+ `${validated.overlappingPairsFound} overlapping pair(s)`);
1391
+ }
1392
+ if (validated.serializationEnabled) {
1393
+ say(` coverage: ${validated.coverageScope}`);
1394
+ if (validated.undeclaredNodes > 0 || validated.undeclaredPairsSkipped > 0) {
1395
+ deps.errorLog(`WARNING: ${validated.undeclaredNodes} node(s) declared no touched_files, so ` +
1396
+ `${validated.undeclaredPairsSkipped} pair(s) were never evaluated. Serialization ` +
1397
+ `proves ordering only for paths BOTH nodes declared — it cannot show that these ` +
1398
+ `tickets will avoid each other.`);
1399
+ }
1400
+ }
1401
+ }
1402
+ // --- Scope-bearing preflight (BAPI-850) ----------------------------------
1403
+ // A FEATURE-BRANCH run that has not yet activated must prove canonical parse
1404
+ // truth BEFORE any ref is changed or any run is created. A reused run that is
1405
+ // already active/blocked carries merged child work on its branch and a
1406
+ // recorded cut; it is never re-cut (its persisted run/scope association is
1407
+ // used for status instead). A no-feature run performs none of this.
1408
+ //
1409
+ // The scope's BASE is the repository's configured base branch (the same
1410
+ // resolution `conduct-epic init` uses), NOT `policy_json.base_branch` — that
1411
+ // key names the FEATURE branch. A feature branch equal to the configured base
1412
+ // needs no scope (dispatch already cuts from it) and matches the server's
1413
+ // long-standing no-op for that case.
1414
+ const existingRunActivated = existingStatus !== null &&
1415
+ existingStatus !== "planning" &&
1416
+ existingStatus !== "pending_approval";
1417
+ let scopeBaseBranch = null;
1418
+ let scopeBearing = false;
1419
+ let cutCommitSha = null;
1420
+ if (effectiveFeatureBranch !== undefined) {
1421
+ const configured = await getConfigFieldBaseBranch(access, deps.fetch);
1422
+ scopeBaseBranch =
1423
+ configured.ok && configured.value.base_branch !== null ? configured.value.base_branch : "main";
1424
+ scopeBearing = effectiveFeatureBranch !== scopeBaseBranch;
1425
+ }
1426
+ const needsCut = scopeBearing && !existingRunActivated;
1427
+ if (needsCut) {
1428
+ const truth = await readCanonicalParseTruth(access, deps.fetch);
1429
+ if (!truth.ok) {
1430
+ for (const line of truth.lines)
1431
+ deps.errorLog(line);
1432
+ return 1;
1433
+ }
1434
+ cutCommitSha = truth.cutCommitSha;
1435
+ say(`Cut: ${cutCommitSha} (canonical indexed commit; origin/${effectiveFeatureBranch} will be cut here)`);
1211
1436
  }
1212
1437
  if (opts.dryRun) {
1213
1438
  say("");
@@ -1220,8 +1445,10 @@ export async function runSetupEpicCli(argv, overrides = {}) {
1220
1445
  else {
1221
1446
  say(` - POST /jira/epic-runs/runs (create run for ${opts.epicKey})`);
1222
1447
  }
1223
- if (featureBranch !== undefined) {
1224
- say(` - feature branch: ${featureBranch} (create from repository base branch; no request made in dry-run)`);
1448
+ if (effectiveFeatureBranch !== undefined) {
1449
+ say(needsCut && cutCommitSha !== null
1450
+ ? ` - 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)`
1451
+ : ` - feature branch: ${effectiveFeatureBranch} (validated server-side at approval; no request made in dry-run)`);
1225
1452
  }
1226
1453
  if (opts.reviewPolicy !== undefined) {
1227
1454
  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 +1464,12 @@ export async function runSetupEpicCli(argv, overrides = {}) {
1237
1464
  local_plan_hash: localHash,
1238
1465
  existing_run_id: existingRunId,
1239
1466
  // Only present for a feature-branch run — no-feature JSON is unchanged.
1240
- ...(featureBranch !== undefined ? { feature_branch: featureBranch } : {}),
1467
+ // BAPI-850: the EFFECTIVE branch, so a policy-file-declared feature run
1468
+ // reports the same key a --feature-branch run does.
1469
+ ...(effectiveFeatureBranch !== undefined ? { feature_branch: effectiveFeatureBranch } : {}),
1470
+ // BAPI-850: the exact commit the cut WOULD use, reported without
1471
+ // cutting. Present only when the dry run validated canonical parse truth.
1472
+ ...(cutCommitSha !== null ? { cut_commit_sha: cutCommitSha } : {}),
1241
1473
  // Only present when a policy was explicitly selected — the
1242
1474
  // no-policy JSON shape is unchanged.
1243
1475
  ...(opts.reviewPolicy !== undefined
@@ -1252,6 +1484,12 @@ export async function runSetupEpicCli(argv, overrides = {}) {
1252
1484
  server_plan_hash: validated.planHash,
1253
1485
  serialization_enabled: validated.serializationEnabled,
1254
1486
  inserted_edges: validated.insertedEdges,
1487
+ // BAPI-848 — machine consumers get the same coverage the human
1488
+ // output carries, so neither can read 0 edges as a verdict.
1489
+ overlapping_pairs_found: validated.overlappingPairsFound,
1490
+ undeclared_nodes: validated.undeclaredNodes,
1491
+ undeclared_pairs_skipped: validated.undeclaredPairsSkipped,
1492
+ coverage_scope: validated.coverageScope,
1255
1493
  }
1256
1494
  : { result: "partial (local checks only)" }),
1257
1495
  warnings,
@@ -1259,6 +1497,46 @@ export async function runSetupEpicCli(argv, overrides = {}) {
1259
1497
  }
1260
1498
  return 0;
1261
1499
  }
1500
+ // --- Local git preflight for the cut (BAPI-850) --------------------------
1501
+ // Still BEFORE any mutating request: the operator's checkout must be able to
1502
+ // reach origin, hold the exact cut object, and origin/<feature> must be absent
1503
+ // or already at the cut. A failure here creates nothing and changes nothing.
1504
+ let cutRunCommand;
1505
+ if (needsCut && cutCommitSha !== null && scopeBaseBranch !== null) {
1506
+ cutRunCommand = deps.runCommand;
1507
+ if (cutRunCommand === undefined) {
1508
+ deps.errorLog(`A feature-branch run cuts origin/${effectiveFeatureBranch} with your local git, but no ` +
1509
+ `command runner is available in this environment. No run was created and no branch was changed.`);
1510
+ return 1;
1511
+ }
1512
+ const gitDeps = cutProtocolDeps(deps, cutRunCommand);
1513
+ const fetched = await runGit(gitDeps, ["fetch", "origin", scopeBaseBranch]);
1514
+ if (fetched.exitCode !== 0) {
1515
+ deps.errorLog(`git fetch origin ${scopeBaseBranch} failed in ${deps.cwd}. Run setup-epic from a clone ` +
1516
+ `of ${access.repoName} with an 'origin' remote. No run was created and no branch was changed.`);
1517
+ return 1;
1518
+ }
1519
+ if (!(await ensureCommitResolvableLocally(gitDeps, cutCommitSha))) {
1520
+ deps.errorLog(`The canonical indexed commit ${cutCommitSha} could not be resolved locally even after ` +
1521
+ `fetching it from origin. Fetch it manually, or re-parse ${access.repoName}. No run was ` +
1522
+ `created and no branch was changed.`);
1523
+ return 1;
1524
+ }
1525
+ const remoteHead = await readRemoteBranchHead(gitDeps, effectiveFeatureBranch);
1526
+ if (!remoteHead.ok) {
1527
+ deps.errorLog(`${remoteHead.error} No run was created and no branch was changed.`);
1528
+ return 1;
1529
+ }
1530
+ if (remoteHead.sha !== null && remoteHead.sha !== cutCommitSha) {
1531
+ deps.errorLog(`origin/${effectiveFeatureBranch} already exists at ${remoteHead.sha}, which is not the ` +
1532
+ `canonical indexed commit ${cutCommitSha}. setup-epic never force-updates an existing ` +
1533
+ `branch: delete it, or finish the previous run first. No run was created and no branch was changed.`);
1534
+ return 1;
1535
+ }
1536
+ say(remoteHead.sha === null
1537
+ ? `Branch: origin/${effectiveFeatureBranch} is absent; it will be cut at ${cutCommitSha}`
1538
+ : `Branch: origin/${effectiveFeatureBranch} already sits at the cut commit ${cutCommitSha}`);
1539
+ }
1262
1540
  const result = {
1263
1541
  epic_run_id: existingRunId ?? "",
1264
1542
  epic_key: opts.epicKey,
@@ -1274,8 +1552,10 @@ export async function runSetupEpicCli(argv, overrides = {}) {
1274
1552
  policy_applied: "absent",
1275
1553
  warnings,
1276
1554
  };
1277
- if (featureBranch !== undefined)
1278
- result.feature_branch = featureBranch;
1555
+ // BAPI-850: the EFFECTIVE feature branch (flag or policy file), so a
1556
+ // policy-file-declared feature run reports the same key a flag run does.
1557
+ if (effectiveFeatureBranch !== undefined)
1558
+ result.feature_branch = effectiveFeatureBranch;
1279
1559
  // --- Step 1: create (only when there is no live run) ---------------------
1280
1560
  if (existingRunId) {
1281
1561
  say(`Run: reusing ${existingRunId} (status: ${existingStatus})`);
@@ -1366,6 +1646,85 @@ export async function runSetupEpicCli(argv, overrides = {}) {
1366
1646
  return 1;
1367
1647
  }
1368
1648
  }
1649
+ // --- Plane-manifest binding (BAPI-872) ------------------------------------
1650
+ // A live `plane up` for THIS exact repository root can now be told which run
1651
+ // it is driving, so a later `plane down` can stop it automatically instead
1652
+ // of guessing from repository-wide active-run state (which risks stopping
1653
+ // the WRONG run). Absence of a local plane — remote runs, CI, any non-plane
1654
+ // workflow — is normal and produces neither an attempt nor a warning.
1655
+ if (deps.planeBinding) {
1656
+ try {
1657
+ const manifestRead = await deps.planeBinding.readManifest(deps.cwd);
1658
+ if (manifestRead.kind === "valid") {
1659
+ const manifest = manifestRead.manifest;
1660
+ const isLive = manifestHasLiveProcess(manifest, {
1661
+ isAlive: deps.planeBinding.isPlaneProcessAlive,
1662
+ });
1663
+ if (isLive) {
1664
+ const bound = await deps.planeBinding.bind(deps.cwd, manifest.planeId, result.epic_run_id);
1665
+ if (bound.ok) {
1666
+ say(bound.alreadyBound
1667
+ ? `Plane: already bound to run ${result.epic_run_id}`
1668
+ : `Plane: bound to run ${result.epic_run_id} — \`plane down\` will stop it automatically`);
1669
+ }
1670
+ else {
1671
+ const msg = `Could not bind the local plane to run ${result.epic_run_id} (${bound.message}). ` +
1672
+ "A later `plane down` cannot be guaranteed to stop it automatically — if you need " +
1673
+ `to stop this run, run \`conductor stop-run --epic-run-id ${result.epic_run_id}\`.`;
1674
+ warnings.push(msg);
1675
+ say(`Plane: [warn] ${msg}`);
1676
+ }
1677
+ }
1678
+ // A recorded-but-not-live manifest is normal: `plane up` will revalidate
1679
+ // and replace a stale record on its own, and there is nothing running to
1680
+ // hand this binding to yet.
1681
+ }
1682
+ // A missing or unvalidated manifest is normal — no attempt, no warning.
1683
+ }
1684
+ catch (err) {
1685
+ // The binding capability itself must never fail setup-epic as a whole;
1686
+ // an unexpected error here is reported as a warning, never thrown.
1687
+ const msg = `Could not check for a local plane to bind run ${result.epic_run_id} to (${errorDetail(err)}). ` +
1688
+ "A later `plane down` cannot be guaranteed to stop it automatically — if you need to stop " +
1689
+ `this run, run \`conductor stop-run --epic-run-id ${result.epic_run_id}\`.`;
1690
+ warnings.push(msg);
1691
+ say(`Plane: [warn] ${msg}`);
1692
+ }
1693
+ }
1694
+ // --- The exact cut (BAPI-850) --------------------------------------------
1695
+ // AFTER the run exists and BEFORE the plan is stored/approved: the scope's run
1696
+ // association is immutable server-side, so the cut is driven with this run's
1697
+ // id and the scope is minted owned by it. Everything from `cut/begin` to the
1698
+ // hold's release is the SHARED protocol `conduct-epic init` drives — local git
1699
+ // only, an exact-SHA create-only push, never the GitHub App, never a force
1700
+ // update. A failure leaves the run non-active (never discoverable by the
1701
+ // reconciler) and re-running setup-epic reuses it and re-drives the cut.
1702
+ let cutScopeId = null;
1703
+ if (needsCut && cutCommitSha !== null && scopeBaseBranch !== null && cutRunCommand !== undefined) {
1704
+ const cut = await performExactIndexScopeCut(cutProtocolDeps(deps, cutRunCommand), access, {
1705
+ featureBranch: effectiveFeatureBranch,
1706
+ baseBranch: scopeBaseBranch,
1707
+ candidateCommitSha: cutCommitSha,
1708
+ epicRunId: result.epic_run_id,
1709
+ });
1710
+ if (!cut.ok) {
1711
+ for (const line of cut.failures)
1712
+ deps.errorLog(line);
1713
+ if (cut.kind === "existing_ref_mismatch" || cut.kind === "confirm_failed") {
1714
+ deps.errorLog(`Expected origin/${effectiveFeatureBranch} at the canonical indexed commit ` +
1715
+ `${cut.expectedSha ?? cutCommitSha}` +
1716
+ (cut.observedSha ? `, found ${cut.observedSha}.` : ".") +
1717
+ ` setup-epic never force-pushes: correct the local/origin branch, then re-run setup-epic.`);
1718
+ }
1719
+ deps.errorLog(`Epic run ${result.epic_run_id} exists but was NOT approved and will not dispatch; ` +
1720
+ `re-running setup-epic reuses it and re-drives the cut.`);
1721
+ return emitRefusal(deps, opts, result);
1722
+ }
1723
+ cutScopeId = cut.lease.scope_id;
1724
+ say(cut.branchCreated
1725
+ ? `Branch: cut origin/${effectiveFeatureBranch} at ${cut.lease.cut_commit_sha} with local git (scope ${cutScopeId})`
1726
+ : `Branch: origin/${effectiveFeatureBranch} confirmed at ${cut.lease.cut_commit_sha} (scope ${cutScopeId}, cut already recorded)`);
1727
+ }
1369
1728
  // --- Step 2/3: store the plan -------------------------------------------
1370
1729
  try {
1371
1730
  const stored = await storeEpicPlan(access, {
@@ -1390,23 +1749,24 @@ export async function runSetupEpicCli(argv, overrides = {}) {
1390
1749
  deps.errorLog(`Failed to store the plan: ${errorDetail(err)}`);
1391
1750
  return 1;
1392
1751
  }
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…`);
1752
+ // --- Step 4: approve (also validates the feature branch server-side) ------
1753
+ // For a feature-branch run the backend READ-validates the branch on origin
1754
+ // against the scope's recorded cut as an approval prerequisite (BAPI-850: the
1755
+ // local cut above created it; the App never creates refs). Validation runs
1756
+ // and completes BEFORE the approval (ticket-seeding + CAS activation) step
1757
+ // server-side, so only a failure the backend tags with the structured
1758
+ // FEATURE_BRANCH_PROVISIONING error_code is actually a branch/scope/credentials
1759
+ // problem key the distinct message off that code, not merely off whether a
1760
+ // feature branch is set, so an unrelated approval failure (e.g.
1761
+ // ticket-seeding, a masked 500) is not misattributed to provisioning.
1762
+ if (effectiveFeatureBranch !== undefined) {
1763
+ say(`Branch: validating origin/${effectiveFeatureBranch} server-side (App read) and preparing the index scope…`);
1404
1764
  }
1405
1765
  const approval = await approveEpicPlan(access, { epicKey: opts.epicKey, planVersion: plan.plan_version }, deps.fetch).catch((err) => {
1406
- if (featureBranch !== undefined &&
1766
+ if (effectiveFeatureBranch !== undefined &&
1407
1767
  err instanceof ConductorBridgeApiError &&
1408
1768
  err.errorCode === "FEATURE_BRANCH_PROVISIONING") {
1409
- deps.errorLog(`Failed to provision the feature branch '${featureBranch}' — child-ticket ` +
1769
+ deps.errorLog(`Failed to provision the feature branch '${effectiveFeatureBranch}' — child-ticket ` +
1410
1770
  `dispatch has NOT started. Correct repository access or the branch ` +
1411
1771
  `configuration, then re-run setup-epic.\nDetail: ${errorDetail(err)}`);
1412
1772
  }
@@ -1426,12 +1786,14 @@ export async function runSetupEpicCli(argv, overrides = {}) {
1426
1786
  if (prov) {
1427
1787
  result.feature_branch_provisioning = prov;
1428
1788
  if (prov.status === "created") {
1789
+ // Legacy servers only: a BAPI-850 server never creates a ref.
1429
1790
  say(`Branch: ready on origin — created '${prov.feature_branch}' from ` +
1430
1791
  `'${prov.source_branch}' at ${prov.source_sha}`);
1431
1792
  }
1432
1793
  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}`);
1794
+ say(`Branch: '${prov.feature_branch}' validated on origin at head ` +
1795
+ `${prov.remote_head_sha} (cut ${prov.source_sha} from '${prov.source_branch}'; ` +
1796
+ `the remote ref was not moved or reset)`);
1435
1797
  }
1436
1798
  }
1437
1799
  }
@@ -1452,6 +1814,93 @@ export async function runSetupEpicCli(argv, overrides = {}) {
1452
1814
  result.warnings.push(`Server plan hash differs from the local hash (the server re-hashes after ` +
1453
1815
  `applying file-overlap serialization). The server hash is authoritative.`);
1454
1816
  }
1817
+ // --- Bounded scope lifecycle feedback (BAPI-850) --------------------------
1818
+ // Only after a feature-bearing approval succeeded (or an idempotent approval
1819
+ // confirmed an already-active run). The scope id comes from THIS invocation's
1820
+ // cut, or — for a reused run whose cut an earlier invocation recorded — from
1821
+ // the run's persisted scope association. Transition prose goes to `say`
1822
+ // (stderr under --json) so stdout stays the single result document. The poll
1823
+ // reuses the pilot's interval, cap, and status vocabulary; `ready` is accepted
1824
+ // only when the server proves indexed == cut.
1825
+ let exitCode = 0;
1826
+ if (scopeBearing) {
1827
+ const scopeId = cutScopeId ?? (await resolveScopeIdForRun(access, deps.fetch, result.epic_run_id));
1828
+ if (scopeId === null) {
1829
+ result.index_scope = {
1830
+ scope_id: null,
1831
+ cut_commit_sha: cutCommitSha,
1832
+ lifecycle_state: null,
1833
+ ready: false,
1834
+ disposition: "not_polled",
1835
+ };
1836
+ say(`Scope: no index scope is associated with run ${result.epic_run_id}; ` +
1837
+ `the reconciler will not dispatch until one is ready. Re-run setup-epic.`);
1838
+ exitCode = 1;
1839
+ }
1840
+ else {
1841
+ say(`Scope: waiting for index scope readiness (bounded)…`);
1842
+ const verdict = await pollIndexScopeLifecycle(cutProtocolDeps(deps, cutRunCommand ?? deps.runCommand ?? createExecFileRunCommand()), access, scopeId, { onTransition: (state, status) => say(describeScopeTransition(state, status)) });
1843
+ if (verdict.kind === "ready") {
1844
+ result.index_scope = {
1845
+ scope_id: scopeId,
1846
+ cut_commit_sha: verdict.status.cut_commit_sha,
1847
+ lifecycle_state: verdict.status.lifecycle_state,
1848
+ ready: true,
1849
+ disposition: "ready",
1850
+ };
1851
+ say(`Scope: ready — the reconciler may dispatch against the epic branch index.`);
1852
+ }
1853
+ else if (verdict.kind === "ready_mismatch") {
1854
+ result.index_scope = {
1855
+ scope_id: scopeId,
1856
+ cut_commit_sha: verdict.status.cut_commit_sha,
1857
+ lifecycle_state: verdict.status.lifecycle_state,
1858
+ ready: false,
1859
+ disposition: "failed",
1860
+ failure_reason: "ready_watermark_mismatch",
1861
+ recovery_command: SETUP_EPIC_SCOPE_RECOVERY_COMMAND,
1862
+ };
1863
+ deps.errorLog(`Index scope ${scopeId} reports ready but its indexed commit ` +
1864
+ `(${verdict.status.indexed_commit_sha ?? "none"}) is not the cut commit ` +
1865
+ `(${verdict.status.cut_commit_sha ?? "none"}). Treating the scope as NOT ready; ` +
1866
+ `ticket dispatch remains blocked by the reconciler's freshness hold. ` +
1867
+ `Recovery: ${SETUP_EPIC_SCOPE_RECOVERY_COMMAND}.`);
1868
+ exitCode = 1;
1869
+ }
1870
+ else if (verdict.kind === "failed") {
1871
+ result.index_scope = {
1872
+ scope_id: scopeId,
1873
+ cut_commit_sha: verdict.status.cut_commit_sha,
1874
+ lifecycle_state: verdict.status.lifecycle_state,
1875
+ ready: false,
1876
+ disposition: "failed",
1877
+ failure_reason: verdict.reason,
1878
+ recovery_command: SETUP_EPIC_SCOPE_RECOVERY_COMMAND,
1879
+ };
1880
+ deps.errorLog(`Index scope ${scopeId} FAILED (reason: ${verdict.reason}) — this is a recorded ` +
1881
+ `failure, not an in-progress state. Ticket dispatch remains blocked: the reconciler ` +
1882
+ `will not dispatch against a failed scope.`);
1883
+ deps.errorLog(scopeFailureGuidance(verdict.reason, effectiveFeatureBranch ?? "", result.epic_run_id));
1884
+ exitCode = 1;
1885
+ }
1886
+ else {
1887
+ result.index_scope = {
1888
+ scope_id: scopeId,
1889
+ cut_commit_sha: verdict.lastStatus?.cut_commit_sha ?? cutCommitSha,
1890
+ lifecycle_state: verdict.lastStatus?.lifecycle_state ?? null,
1891
+ ready: false,
1892
+ disposition: "timeout",
1893
+ };
1894
+ deps.errorLog(`Index scope ${scopeId} did not become ready within the bounded wait ` +
1895
+ `(last observed state: ${verdict.lastState}). The run ${result.epic_run_id} is ` +
1896
+ `created and scope preparation continues asynchronously on the server; the ` +
1897
+ `reconciler will NOT dispatch tickets until the scope becomes ready. Check ` +
1898
+ `GET /jira/index-scope/status?repo_name=${access.repoName}&scope_id=${scopeId}, ` +
1899
+ `or re-run setup-epic to wait again.`);
1900
+ exitCode = 1;
1901
+ }
1902
+ }
1903
+ }
1455
1904
  if (opts.json) {
1456
1905
  deps.log(JSON.stringify(result, null, 2));
1457
1906
  }
@@ -1468,7 +1917,7 @@ export async function runSetupEpicCli(argv, overrides = {}) {
1468
1917
  say(`Epic run ${result.epic_run_id} is ${result.status ?? "unknown"}.`);
1469
1918
  say("The server-side reconciler will pick it up within ~30s.");
1470
1919
  say("To execute claimed jobs on this machine, run:");
1471
- say(` npx -y @bridge_gpt/mcp-server executor --repo ${access.repoName}`);
1920
+ say(` npx -y ${MCP_PACKAGE_NAME} executor --repo ${access.repoName}`);
1472
1921
  }
1473
- return 0;
1922
+ return exitCode;
1474
1923
  }