@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.
- package/README.md +330 -191
- package/build/agent-capabilities/cli.js +2 -1
- package/build/agent-launchers/claude-executor-adapter.js +17 -4
- package/build/agents.generated.js +2 -2
- package/build/claude-review-workflow.js +510 -45
- package/build/claude-user-config-doctor.js +42 -11
- package/build/cli-release.js +2 -1
- package/build/commands.generated.js +6 -5
- package/build/conduct-epic/bridge-client.js +354 -113
- package/build/conduct-epic/checkpoint-store.js +17 -0
- package/build/conduct-epic/cli.js +947 -99
- package/build/conduct-epic/cut-protocol.js +327 -0
- package/build/conduct-epic/spawn.js +14 -2
- package/build/conductor/bridge-api-client.js +148 -1
- package/build/conductor/cli.js +109 -1
- package/build/conductor/doctor.js +101 -16
- package/build/conductor/epic-reconcile.js +72 -19
- package/build/conductor/epic-runtime.js +15 -3
- package/build/conductor/errors.js +47 -0
- package/build/conductor/git-hooks.js +205 -11
- package/build/conductor/install-doctor.js +230 -1
- package/build/conductor/local-merge.js +130 -28
- package/build/conductor/recovery-cli.js +313 -0
- package/build/conductor/recovery-operations.js +219 -0
- package/build/conductor/tools.js +32 -3
- package/build/conductor/worker-ledger-cli.js +27 -1
- package/build/conductor-bin.js +20 -16
- package/build/credentials-cli.js +3 -2
- package/build/docs.generated.js +2 -1
- package/build/doctor.js +120 -44
- package/build/drive-epic.js +375 -0
- package/build/executor/cli.js +48 -1
- package/build/executor/env.js +21 -0
- package/build/executor/http-client.js +71 -3
- package/build/executor/index-scope.js +39 -0
- package/build/executor/job-errors.js +9 -0
- package/build/executor/job-log-registry.js +69 -0
- package/build/executor/job-runner.js +198 -29
- package/build/executor/live-worker-registry.js +83 -0
- package/build/executor/observation.js +259 -6
- package/build/executor/platform.js +147 -3
- package/build/executor/process.js +58 -14
- package/build/executor/runner.js +454 -48
- package/build/executor/test-clock.js +3 -2
- package/build/executor/worker-finalization.js +233 -56
- package/build/executor/worktree.js +8 -1
- package/build/index-scope-contract.js +96 -0
- package/build/index.js +2277 -270
- package/build/init.js +83 -22
- package/build/install-bridge-conductor.js +323 -14
- package/build/install-bridge.js +225 -47
- package/build/install-doctor.js +23 -9
- package/build/install-reexec.js +2 -1
- package/build/launcher-config-inspection.js +83 -22
- package/build/mcp-host-config.js +331 -67
- package/build/mcp-host-targets.js +45 -21
- package/build/mcp-identity.js +92 -0
- package/build/mcp-install-state.js +94 -1
- package/build/mcp-invoke.js +2 -1
- package/build/mcp-provisioning.js +45 -12
- package/build/mcp-registration-doctor.js +35 -13
- package/build/mcp-server-invocation.js +4 -2
- package/build/merge-pull-request.js +208 -9
- package/build/pipelines.generated.js +305 -15
- package/build/plane/cli.js +73 -7
- package/build/plane/defaults.js +18 -5
- package/build/plane/manifest.js +90 -0
- package/build/plane/preflight.js +100 -10
- package/build/plane/shutdown.js +71 -3
- package/build/plane/test-fakes.js +9 -1
- package/build/readme.generated.js +1 -1
- package/build/regression-check.js +3 -2
- package/build/review-tickets.js +8 -7
- package/build/run-unit-tests-launcher.js +149 -6
- package/build/schedule-run.js +3 -2
- package/build/setup-epic.js +531 -82
- package/build/sfcc/tool-wrapper.js +15 -0
- package/build/start-tickets-prereqs.js +11 -6
- package/build/start-tickets.js +91 -85
- package/build/update-check.js +3 -2
- package/build/upgrade-advice.js +2 -1
- package/build/upgrade-cli.js +50 -18
- package/build/version.generated.js +2 -1
- package/build/worktree-core.js +31 -17
- package/docs/CONDUCTOR.md +22 -0
- package/docs/install/mcp-tool-integrations.md +19 -3
- package/package.json +2 -2
- package/pipelines/greenfield-setup.json +286 -0
package/build/setup-epic.js
CHANGED
|
@@ -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
|
-
*
|
|
34
|
-
*
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
"
|
|
133
|
-
"
|
|
134
|
-
"
|
|
135
|
-
"
|
|
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
|
-
|
|
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} (
|
|
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
|
-
`
|
|
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
|
-
`
|
|
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
|
-
|
|
1210
|
-
|
|
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 (
|
|
1224
|
-
say(
|
|
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
|
-
|
|
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
|
-
|
|
1278
|
-
|
|
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
|
|
1394
|
-
// For a feature-branch run the backend
|
|
1395
|
-
// as an approval prerequisite
|
|
1396
|
-
//
|
|
1397
|
-
//
|
|
1398
|
-
//
|
|
1399
|
-
//
|
|
1400
|
-
//
|
|
1401
|
-
//
|
|
1402
|
-
|
|
1403
|
-
|
|
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 (
|
|
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 '${
|
|
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}'
|
|
1434
|
-
|
|
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
|
|
1920
|
+
say(` npx -y ${MCP_PACKAGE_NAME} executor --repo ${access.repoName}`);
|
|
1472
1921
|
}
|
|
1473
|
-
return
|
|
1922
|
+
return exitCode;
|
|
1474
1923
|
}
|