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