@bridge_gpt/mcp-server 0.2.50 → 0.2.52
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 +24 -8
- package/build/agent-capabilities/probe-context.js +15 -7
- package/build/agent-capabilities/probes.js +42 -6
- package/build/agent-launchers/claude-executor-adapter.js +98 -14
- package/build/commands.generated.js +1 -1
- package/build/conduct-epic/bridge-client.js +115 -1
- package/build/conduct-epic/cli.js +351 -33
- package/build/conduct-epic/cut-protocol.js +65 -0
- package/build/conductor/bridge-api-client.js +171 -5
- package/build/conductor/deny-enforcement-preflight.js +107 -10
- package/build/conductor/local-merge.js +170 -11
- package/build/conductor-bin.js +2 -2
- package/build/connect-bitbucket-api.js +370 -0
- package/build/connect-bitbucket.js +437 -0
- package/build/docs.generated.js +1 -1
- package/build/doctor.js +230 -1
- package/build/drive-epic.js +423 -11
- package/build/env-file-link.js +164 -0
- package/build/epic-integration-pr.js +290 -0
- package/build/executor/cli.js +41 -6
- package/build/executor/deps.js +5 -1
- package/build/executor/env-file-guard.js +113 -0
- package/build/executor/env.js +78 -1
- package/build/executor/heartbeat.js +9 -0
- package/build/executor/http-client.js +90 -22
- package/build/executor/job-errors.js +43 -2
- package/build/executor/job-runner.js +137 -29
- package/build/executor/merge-job.js +102 -6
- package/build/executor/permissions.js +106 -0
- package/build/executor/preflight.js +38 -13
- package/build/executor/resume-pre-spawn.js +2 -1
- package/build/executor/runner.js +175 -4
- package/build/executor/service-unit.js +15 -0
- package/build/executor/terminal-mutation.js +22 -1
- package/build/executor/types.js +86 -0
- package/build/executor/worker-command.js +21 -5
- package/build/executor/worker-guard-hook.js +939 -0
- package/build/executor/worker-log.js +56 -0
- package/build/executor/worktree.js +11 -0
- package/build/git-reachability.js +147 -0
- package/build/index.js +535 -95
- package/build/install-bridge.js +95 -0
- package/build/pipelines.generated.js +10 -2
- package/build/plan-epic-conductor-eligibility.js +213 -0
- package/build/plane/cli.js +78 -15
- package/build/plane/defaults.js +165 -0
- package/build/plane/manifest.js +63 -8
- package/build/plane/member-logs.js +6 -0
- package/build/plane/member-roster.js +195 -11
- package/build/plane/preflight.js +43 -0
- package/build/plane/shutdown.js +25 -3
- package/build/plane/status.js +11 -0
- package/build/plane/supervisor.js +343 -14
- package/build/plane/test-fakes.js +43 -0
- package/build/plane/types.js +82 -11
- package/build/pr-base-contract.js +20 -0
- package/build/readme.generated.js +1 -1
- package/build/review-synthesis-config.js +60 -0
- package/build/scripts/executor-protocol-contract-driver.js +311 -0
- package/build/setup-epic.js +592 -139
- package/build/sfcc/log-query.js +2 -1
- package/build/sfcc/reads-custom-object-def.js +10 -13
- package/build/sfcc/reads-site-preference.js +5 -5
- package/build/sfcc/reads-system-object.js +4 -4
- package/build/sfcc/writes-custom-object-def.js +7 -7
- package/build/sfcc/writes-site-preference.js +4 -3
- package/build/sfcc/writes-system-object.js +7 -6
- package/build/start-tickets-conductor.js +11 -2
- package/build/start-tickets.js +69 -2
- package/build/version.generated.js +3 -3
- package/build/worker-containment-diagnostic.js +97 -0
- package/build/worker-guard-hook-bin.js +6 -0
- package/docs/CONDUCTOR.md +27 -0
- package/docs/install/mcp-tool-integrations.md +3 -2
- package/package.json +5 -3
- package/pipelines/plan-epic.json +5 -0
package/build/setup-epic.js
CHANGED
|
@@ -30,9 +30,15 @@
|
|
|
30
30
|
* file-overlap serialization, so a divergence is legitimate, not corruption.
|
|
31
31
|
* 5. Store, then approve.
|
|
32
32
|
*
|
|
33
|
+
* BAPI-1009 — the epic branch is the DEFAULT for a multi-ticket epic. A plan
|
|
34
|
+
* with two or more nodes and no branch flag derives `epic/<KEY>` on EVERY path
|
|
35
|
+
* (piped, CI, and `--json` included), so the scope-bearing sequence below is now
|
|
36
|
+
* the ordinary path rather than an opt-in one. A single-node plan stays on the
|
|
37
|
+
* repository base branch, and `--into-base` forces base for any node count.
|
|
38
|
+
*
|
|
33
39
|
* BAPI-850 — a FEATURE-BRANCH run (`policy_json.base_branch` set, whether by
|
|
34
|
-
* `--feature-branch
|
|
35
|
-
* start sequence in this one invocation, in this order:
|
|
40
|
+
* the BAPI-1009 default, `--feature-branch`, or `--policy-file`) performs the
|
|
41
|
+
* full scope-bearing start sequence in this one invocation, in this order:
|
|
36
42
|
*
|
|
37
43
|
* a. Read canonical parse truth (`GET /jira/parse-status`). No succeeded
|
|
38
44
|
* parse or no indexed commit → exit 1 with "parse the repository first";
|
|
@@ -47,13 +53,18 @@
|
|
|
47
53
|
* d. Store and approve (unchanged requests). The server now READ-validates the
|
|
48
54
|
* ref against the scope's recorded cut and schedules the seed + verifying
|
|
49
55
|
* parse in the background; the run goes `active` immediately.
|
|
50
|
-
* e. Poll the scope's lifecycle with a bounded wait
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
56
|
+
* e. Poll the scope's lifecycle with a SHORT bounded wait (BAPI-1054:
|
|
57
|
+
* {@link SETUP_EPIC_SCOPE_DEFERRED_MAX_POLLS} polls, ~30s), reporting
|
|
58
|
+
* `Provisioning → Seeding → Verifying → Ready`. A scope still preparing at
|
|
59
|
+
* that deadline is acknowledged as `created, cut deferred` and exits 0 —
|
|
60
|
+
* the run is created and approved, and preparation continues server-side.
|
|
61
|
+
* Only `failed` and `ready_mismatch` are terminal and exit 1. The
|
|
62
|
+
* reconciler's dispatch-freshness hold keeps an un-ready scope from
|
|
63
|
+
* dispatching in every case, which is what makes the deferred return safe.
|
|
54
64
|
*
|
|
55
|
-
* A run with no feature branch
|
|
56
|
-
* `git`, no scope, no polling, no
|
|
65
|
+
* A run with no feature branch — a single-node plan, or `--into-base` — is
|
|
66
|
+
* byte-identical to before: no parse read, no `git`, no scope, no polling, no
|
|
67
|
+
* extra JSON keys.
|
|
57
68
|
*
|
|
58
69
|
* Reads the plan sidecar and the optional policy file from the local
|
|
59
70
|
* filesystem. As of BAPI-872 it can also WRITE the local filesystem in one
|
|
@@ -66,8 +77,9 @@
|
|
|
66
77
|
*/
|
|
67
78
|
import { readFile as fsReadFile, stat as fsStat } from "node:fs/promises";
|
|
68
79
|
import os from "node:os";
|
|
69
|
-
import
|
|
80
|
+
import { createProductionEpicIntegrationGhRunner, ensureEpicIntegrationPullRequest, formatEpicIntegrationPullRequestOutcome, } from "./epic-integration-pr.js";
|
|
70
81
|
import { approveEpicPlan, createEpicRunWithDisposition, fetchEpicRunState, replaceEpicRunPolicy, resolveConductorBridgeApiAccess, storeEpicPlan, validateEpicPlan, ConductorBridgeApiError, } from "./conductor/bridge-api-client.js";
|
|
82
|
+
import { resolveTrackedPaths } from "./git-reachability.js";
|
|
71
83
|
import { validateBranchName } from "./base-ref.js";
|
|
72
84
|
// Reused from the conduct-epic client on purpose: adding a second wrapper under
|
|
73
85
|
// `conductor/` would give the v2 path its own copy to drift from.
|
|
@@ -78,7 +90,7 @@ import { validateBranchName } from "./base-ref.js";
|
|
|
78
90
|
// conductor can drift on a field name or an endpoint shape. No MCP tool, no
|
|
79
91
|
// input-schema field, and no description changes.
|
|
80
92
|
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";
|
|
93
|
+
import { boundScopeLifecycleLabel, createExecFileRunCommand, ensureCommitResolvableLocally, normalizeCommitSha, performExactIndexScopeCut, pollIndexScopeLifecycle, readRemoteBranchHead, runGit, SCOPE_BOOTSTRAP_POLL_INTERVAL_MS, SCOPE_LIFECYCLE_LABELS, } from "./conduct-epic/cut-protocol.js";
|
|
82
94
|
import { hashPlan } from "./conductor/plan.js";
|
|
83
95
|
import { MCP_PACKAGE_NAME } from "./mcp-identity.js";
|
|
84
96
|
// BAPI-872: local plane-manifest binding, so `plane down` can later stop the
|
|
@@ -92,25 +104,6 @@ export const SETUP_EPIC_REVIEW_POLICY_SOURCES = [
|
|
|
92
104
|
"native_review_decision",
|
|
93
105
|
"none",
|
|
94
106
|
];
|
|
95
|
-
/** Echoed single-line prompt on stderr (mirrors connect-github's helper). */
|
|
96
|
-
function defaultPromptLine(promptText) {
|
|
97
|
-
return new Promise((resolve) => {
|
|
98
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
99
|
-
// `rl.close()` emits `close` SYNCHRONOUSLY, so without this guard the close
|
|
100
|
-
// handler would settle the promise empty and discard a real answer. EOF must
|
|
101
|
-
// also resolve rather than deadlock a top-level await.
|
|
102
|
-
let answered = false;
|
|
103
|
-
rl.on("close", () => {
|
|
104
|
-
if (!answered)
|
|
105
|
-
resolve("");
|
|
106
|
-
});
|
|
107
|
-
rl.question(promptText, (answer) => {
|
|
108
|
-
answered = true;
|
|
109
|
-
rl.close();
|
|
110
|
-
resolve(answer.trim());
|
|
111
|
-
});
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
107
|
export function createDefaultSetupEpicDeps() {
|
|
115
108
|
return {
|
|
116
109
|
env: process.env,
|
|
@@ -122,9 +115,8 @@ export function createDefaultSetupEpicDeps() {
|
|
|
122
115
|
fetch: globalThis.fetch,
|
|
123
116
|
log: (m) => console.log(m),
|
|
124
117
|
errorLog: (m) => console.error(m),
|
|
125
|
-
isTTY: Boolean(process.stdin.isTTY),
|
|
126
|
-
promptLine: defaultPromptLine,
|
|
127
118
|
runCommand: createExecFileRunCommand(),
|
|
119
|
+
epicIntegrationGh: createProductionEpicIntegrationGhRunner(),
|
|
128
120
|
planeBinding: {
|
|
129
121
|
readManifest: (repoRoot) => readPlaneManifest(repoRoot, createPlaneFsDeps()),
|
|
130
122
|
isPlaneProcessAlive: (pid) => createPlaneProcessDeps().isAlive(pid),
|
|
@@ -140,6 +132,12 @@ export function getSetupEpicUsage() {
|
|
|
140
132
|
"Bootstraps an Epic Conductor v2 run: creates the run, stores the plan DAG,",
|
|
141
133
|
"and approves it. Idempotent — re-running reuses an existing live run.",
|
|
142
134
|
"",
|
|
135
|
+
"Branch strategy (default): a plan with TWO OR MORE nodes runs on a dedicated",
|
|
136
|
+
"epic branch named epic/<KEY>, derived automatically on every path (piped, CI,",
|
|
137
|
+
"and --json included). Every child-ticket PR targets epic/<KEY>, and one DRAFT",
|
|
138
|
+
"integration PR targets the repository base branch. A single-node plan keeps the",
|
|
139
|
+
"repository base branch, matching plain start-tickets semantics.",
|
|
140
|
+
"",
|
|
143
141
|
"Required:",
|
|
144
142
|
" --epic-key <KEY> Jira epic key (e.g. BAPI-405)",
|
|
145
143
|
" --plan-file <path> Path to epic-plan.dag.json (from decompose-epic)",
|
|
@@ -147,15 +145,26 @@ export function getSetupEpicUsage() {
|
|
|
147
145
|
"Options:",
|
|
148
146
|
" --repo <name> Repo name (default: BAPI_REPO_NAME or .bridge/config)",
|
|
149
147
|
" --plan-version <n> Assert the sidecar's plan_version equals <n>",
|
|
150
|
-
" --feature-branch <name>
|
|
148
|
+
" --feature-branch <name> OVERRIDE the derived epic/<KEY> name with your own.",
|
|
151
149
|
" setup-epic cuts the branch on origin with your LOCAL git",
|
|
152
150
|
" at exactly the repository's canonical indexed commit",
|
|
153
151
|
" (the repository must be parsed first), the server",
|
|
154
152
|
" validates it, and every child-ticket PR targets it.",
|
|
155
|
-
" setup-epic then waits (
|
|
156
|
-
|
|
157
|
-
"
|
|
158
|
-
|
|
153
|
+
" setup-epic then waits BRIEFLY (about " +
|
|
154
|
+
`${SETUP_EPIC_SCOPE_DEFERRED_WINDOW_SECONDS}s) for the run's index scope to`,
|
|
155
|
+
" become ready. If it is still preparing at that point the",
|
|
156
|
+
` command exits 0 with a '${SETUP_EPIC_CUT_DEFERRED_PHRASE}'`,
|
|
157
|
+
" acknowledgement naming the run and scope: the run IS",
|
|
158
|
+
" created and approved, preparation continues server-side,",
|
|
159
|
+
" and the reconciler holds ticket dispatch until it is",
|
|
160
|
+
" ready. Only a recorded scope failure exits non-zero.",
|
|
161
|
+
" Also forces an epic branch for a SINGLE-node plan, which",
|
|
162
|
+
" the default would leave on base.",
|
|
163
|
+
" --into-base OPT OUT of the epic-branch default: cut no branch and",
|
|
164
|
+
" target every child PR at the repository base branch",
|
|
165
|
+
" (the pre-BAPI-1009 behavior). Use this if automation",
|
|
166
|
+
" depended on \"no flag = direct to base\". Contradictory",
|
|
167
|
+
" with --feature-branch; passing both is a parse error.",
|
|
159
168
|
" --review-policy <src> PER-RUN review policy source, one of:",
|
|
160
169
|
` ${SETUP_EPIC_REVIEW_POLICY_SOURCES.join(", ")}.`,
|
|
161
170
|
" Composed into policy_json.review_policy on create.",
|
|
@@ -254,6 +263,12 @@ export function parseSetupEpicArgs(argv) {
|
|
|
254
263
|
let repo;
|
|
255
264
|
let planVersion;
|
|
256
265
|
let featureBranch;
|
|
266
|
+
// BAPI-1009: tracked SEPARATELY from `featureBranch`, because an explicitly
|
|
267
|
+
// blank `--feature-branch=` normalizes the value to `undefined` while still
|
|
268
|
+
// being a supplied branch-strategy intent. The contradiction guard below must
|
|
269
|
+
// fire on the INTENT, not on whether a name survived normalization.
|
|
270
|
+
let featureBranchSupplied = false;
|
|
271
|
+
let intoBase = false;
|
|
257
272
|
let reviewPolicy;
|
|
258
273
|
let policyFile;
|
|
259
274
|
let replacePolicy = false;
|
|
@@ -276,6 +291,7 @@ export function parseSetupEpicArgs(argv) {
|
|
|
276
291
|
if (!parsedFb.ok)
|
|
277
292
|
return { status: "error", message: parsedFb.error };
|
|
278
293
|
featureBranch = parsedFb.value;
|
|
294
|
+
featureBranchSupplied = true;
|
|
279
295
|
continue;
|
|
280
296
|
}
|
|
281
297
|
// `--review-policy=<source>` inline form (BAPI-679).
|
|
@@ -296,6 +312,7 @@ export function parseSetupEpicArgs(argv) {
|
|
|
296
312
|
if (!parsedFb.ok)
|
|
297
313
|
return { status: "error", message: parsedFb.error };
|
|
298
314
|
featureBranch = parsedFb.value;
|
|
315
|
+
featureBranchSupplied = true;
|
|
299
316
|
i++;
|
|
300
317
|
break;
|
|
301
318
|
}
|
|
@@ -359,6 +376,9 @@ export function parseSetupEpicArgs(argv) {
|
|
|
359
376
|
i++;
|
|
360
377
|
break;
|
|
361
378
|
}
|
|
379
|
+
case "--into-base":
|
|
380
|
+
intoBase = true;
|
|
381
|
+
break;
|
|
362
382
|
case "--replace-policy":
|
|
363
383
|
replacePolicy = true;
|
|
364
384
|
break;
|
|
@@ -393,6 +413,18 @@ export function parseSetupEpicArgs(argv) {
|
|
|
393
413
|
"on a real setup.",
|
|
394
414
|
};
|
|
395
415
|
}
|
|
416
|
+
// BAPI-1009: the two branch-strategy flags are contradictory intents, refused
|
|
417
|
+
// HERE alongside the other pre-flight contradictions — before any file read,
|
|
418
|
+
// credential resolution, git command, or network request.
|
|
419
|
+
if (featureBranchSupplied && intoBase) {
|
|
420
|
+
return {
|
|
421
|
+
status: "error",
|
|
422
|
+
message: "--feature-branch and --into-base select contradictory branch strategies: " +
|
|
423
|
+
"--feature-branch runs the epic on a named epic branch, --into-base runs " +
|
|
424
|
+
"children directly into the repository base branch. Only one branch strategy " +
|
|
425
|
+
"may be selected — remove whichever one you did not mean.",
|
|
426
|
+
};
|
|
427
|
+
}
|
|
396
428
|
if (replacePolicy && policyFile === undefined) {
|
|
397
429
|
return {
|
|
398
430
|
status: "error",
|
|
@@ -409,6 +441,7 @@ export function parseSetupEpicArgs(argv) {
|
|
|
409
441
|
repo,
|
|
410
442
|
planVersion,
|
|
411
443
|
featureBranch,
|
|
444
|
+
intoBase,
|
|
412
445
|
reviewPolicy,
|
|
413
446
|
policyFile,
|
|
414
447
|
replacePolicy,
|
|
@@ -522,10 +555,16 @@ export function composeSetupEpicPolicy(filePolicy, selections) {
|
|
|
522
555
|
};
|
|
523
556
|
}
|
|
524
557
|
if (fileReview.kind === "source" && fileReview.value !== selections.reviewPolicy) {
|
|
558
|
+
// BAPI-1054: the file's value is NOT echoed. `review_policy.source` is an
|
|
559
|
+
// arbitrary string on the wire, read straight out of an operator-authored
|
|
560
|
+
// file that can also carry `notify.webhook_url` — so interpolating it put
|
|
561
|
+
// unbounded file-derived text into stderr and CI logs. Naming the field and
|
|
562
|
+
// the CLI selection (which came from a closed vocabulary and is therefore
|
|
563
|
+
// safe) says everything the operator needs to fix it.
|
|
525
564
|
return {
|
|
526
565
|
ok: false,
|
|
527
566
|
error: `--review-policy selected '${selections.reviewPolicy}', but --policy-file ` +
|
|
528
|
-
`declares review_policy.source
|
|
567
|
+
`declares a different review_policy.source. setup-epic will not pick ` +
|
|
529
568
|
`a winner between them: drop the flag, or fix the policy file so both agree.`,
|
|
530
569
|
};
|
|
531
570
|
}
|
|
@@ -539,6 +578,53 @@ export function composeSetupEpicPolicy(filePolicy, selections) {
|
|
|
539
578
|
}
|
|
540
579
|
return { ok: true, policy: composed };
|
|
541
580
|
}
|
|
581
|
+
/**
|
|
582
|
+
* The single named policy gate, run before any credential resolution or request.
|
|
583
|
+
*
|
|
584
|
+
* Pure, and deterministic in its refusal order: it reports the FIRST violation
|
|
585
|
+
* and stops, so one invocation yields one named reason. It consolidates checks
|
|
586
|
+
* that already existed but were scattered across two call sites, which is what
|
|
587
|
+
* let `--local-only --dry-run` and an ordinary run evaluate slightly different
|
|
588
|
+
* sets — a dry run is worth nothing if it does not refuse what the real run will.
|
|
589
|
+
*
|
|
590
|
+
* It is READ-ONLY with respect to policy content: nothing here normalizes,
|
|
591
|
+
* coerces, deletes, or reconstructs a policy file's keys.
|
|
592
|
+
* {@link composeSetupEpicPolicy} remains the only function that adds validated
|
|
593
|
+
* CLI selections, and it is called from here so the object that passes the gate
|
|
594
|
+
* is exactly the object the request later carries.
|
|
595
|
+
*
|
|
596
|
+
* Every message is secret-free by construction: fixed prose, the flag names, the
|
|
597
|
+
* file PATH, and CLI-supplied values that came from a closed vocabulary. No
|
|
598
|
+
* message interpolates a value read out of the policy file.
|
|
599
|
+
*/
|
|
600
|
+
export function preflightSetupEpicPolicy(args) {
|
|
601
|
+
// 1. Branch strategy. `--feature-branch` versus `--into-base` is already a
|
|
602
|
+
// parse error, refused before a file is even read; this is the same
|
|
603
|
+
// contradiction spelled across two sources instead of two flags.
|
|
604
|
+
if (args.intoBase && args.policyDeclaresBranch) {
|
|
605
|
+
return {
|
|
606
|
+
ok: false,
|
|
607
|
+
reason: "branch_strategy_contradiction",
|
|
608
|
+
message: `--into-base runs children directly into the repository base branch, but ` +
|
|
609
|
+
`--policy-file '${args.policyFile}' declares a base_branch, which asks for an ` +
|
|
610
|
+
`epic branch. setup-epic will not pick a winner between them: remove the ` +
|
|
611
|
+
`branch declaration from the policy file, or drop --into-base.`,
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
// 2. Policy-source contradictions between the file and the CLI selections.
|
|
615
|
+
// Delegated to the composer, which owns the "no silent winner" rule; the
|
|
616
|
+
// gate only gives the outcome a stable name.
|
|
617
|
+
if (args.filePolicy === undefined)
|
|
618
|
+
return { ok: true, policy: undefined };
|
|
619
|
+
const composed = composeSetupEpicPolicy(args.filePolicy, {
|
|
620
|
+
featureBranch: args.featureBranch,
|
|
621
|
+
reviewPolicy: args.reviewPolicy,
|
|
622
|
+
});
|
|
623
|
+
if (!composed.ok) {
|
|
624
|
+
return { ok: false, reason: "policy_source_contradiction", message: composed.error };
|
|
625
|
+
}
|
|
626
|
+
return { ok: true, policy: composed.policy };
|
|
627
|
+
}
|
|
542
628
|
/**
|
|
543
629
|
* Does this policy object explicitly supply the `notify.webhook_url` leaf?
|
|
544
630
|
*
|
|
@@ -773,6 +859,64 @@ export function validateEpicPlanSidecar(parsed) {
|
|
|
773
859
|
}
|
|
774
860
|
return { ok: true, plan: parsed, warnings };
|
|
775
861
|
}
|
|
862
|
+
/**
|
|
863
|
+
* The cited-document paths the server reported it could NOT resolve (BAPI-1027).
|
|
864
|
+
*
|
|
865
|
+
* Read off the server's own notice rather than re-derived from the plan: the
|
|
866
|
+
* server decides which citations matter and which it still needs evidence for,
|
|
867
|
+
* so the CLI resolves exactly what it was asked for and nothing more. That is
|
|
868
|
+
* what keeps the rule un-mirrored — the CLI reads a field, it does not reimplement
|
|
869
|
+
* a check.
|
|
870
|
+
*/
|
|
871
|
+
export function unresolvedCitedDocPaths(result) {
|
|
872
|
+
const paths = [];
|
|
873
|
+
const seen = new Set();
|
|
874
|
+
for (const notice of result.notices) {
|
|
875
|
+
if (notice.noticeCode !== CITED_DOCUMENT_REACHABILITY_NOT_EVALUATED)
|
|
876
|
+
continue;
|
|
877
|
+
for (const path of notice.paths) {
|
|
878
|
+
if (seen.has(path))
|
|
879
|
+
continue;
|
|
880
|
+
seen.add(path);
|
|
881
|
+
paths.push(path);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
return paths;
|
|
885
|
+
}
|
|
886
|
+
/** The server-side notice code naming citations that still need evidence. */
|
|
887
|
+
const CITED_DOCUMENT_REACHABILITY_NOT_EVALUATED = "CITED_DOCUMENT_REACHABILITY_NOT_EVALUATED";
|
|
888
|
+
/** The server-side TERMINAL duplicate-active-run code (BAPI-1027). */
|
|
889
|
+
export const ACTIVE_EPIC_RUN_EXISTS_CODE = "ACTIVE_EPIC_RUN_EXISTS";
|
|
890
|
+
/**
|
|
891
|
+
* True for a RETRYABLE optimistic-concurrency conflict (BAPI-1016 / BAPI-1027).
|
|
892
|
+
*
|
|
893
|
+
* Two conflict families reach this CLI and they call for opposite advice. A
|
|
894
|
+
* stale-CAS `row_version` means "re-read and retry"; a duplicate active run means
|
|
895
|
+
* "close or adopt, retrying is futile". They are told apart by the server's own
|
|
896
|
+
* `error_code`, never by HTTP status — the terminal one is a 409 and the
|
|
897
|
+
* retryable one is a 400 on these routes, which is precisely the sort of
|
|
898
|
+
* incidental detail that must not be what a renderer depends on.
|
|
899
|
+
*/
|
|
900
|
+
export function isRetryableConflict(err) {
|
|
901
|
+
return err instanceof ConductorBridgeApiError && err.errorCode === "CONFLICT";
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* Render the ` (BAPI-1, BAPI-2 · docs/x.md)` subject of one rejection line.
|
|
905
|
+
*
|
|
906
|
+
* Kept to identifiers the operator needs to act — node keys, a path, a revision
|
|
907
|
+
* identifier. Specification content, credentials, and raw HTTP bodies never
|
|
908
|
+
* appear here.
|
|
909
|
+
*/
|
|
910
|
+
export function renderRejectionSubject(rejection) {
|
|
911
|
+
const parts = [];
|
|
912
|
+
if (rejection.ticketKeys.length > 0)
|
|
913
|
+
parts.push(rejection.ticketKeys.join(", "));
|
|
914
|
+
if (rejection.migrationId)
|
|
915
|
+
parts.push(`revision ${rejection.migrationId}`);
|
|
916
|
+
else if (rejection.path)
|
|
917
|
+
parts.push(rejection.path);
|
|
918
|
+
return parts.length > 0 ? ` (${parts.join(" · ")})` : "";
|
|
919
|
+
}
|
|
776
920
|
/** Iterative DFS cycle detection; returns the offending path or null. */
|
|
777
921
|
function findCycle(keys, adjacency) {
|
|
778
922
|
const WHITE = 0;
|
|
@@ -807,58 +951,85 @@ function findCycle(keys, adjacency) {
|
|
|
807
951
|
return null;
|
|
808
952
|
}
|
|
809
953
|
/** Propose a conductor epic feature branch name for an epic key (BAPI-655). */
|
|
810
|
-
function proposeFeatureBranchName(epicKey) {
|
|
954
|
+
export function proposeFeatureBranchName(epicKey) {
|
|
811
955
|
return `epic/${epicKey}`;
|
|
812
956
|
}
|
|
957
|
+
/**
|
|
958
|
+
* The plan-node count at or above which a no-flag run derives its own epic
|
|
959
|
+
* branch (BAPI-1009, R-4).
|
|
960
|
+
*
|
|
961
|
+
* A single-node plan is not an epic in any meaningful sense — it is one ticket,
|
|
962
|
+
* and plain `start-tickets` puts one ticket's PR straight onto the repository
|
|
963
|
+
* base branch. Cutting an `epic/<KEY>` branch, seeding a per-epic index scope,
|
|
964
|
+
* and opening an integration PR for it would add a whole provisioning sequence
|
|
965
|
+
* (and a second PR to merge) for no isolation benefit. Two or more nodes is
|
|
966
|
+
* where children can depend on one another, which is exactly the case the epic
|
|
967
|
+
* branch exists to serve.
|
|
968
|
+
*/
|
|
969
|
+
export const EPIC_BRANCH_MIN_PLAN_NODES = 2;
|
|
813
970
|
/**
|
|
814
971
|
* Resolve the epic feature branch selection for this invocation.
|
|
815
972
|
*
|
|
816
|
-
* Returns a
|
|
817
|
-
*
|
|
973
|
+
* Returns a branch name, or `undefined` to continue on the repository base
|
|
974
|
+
* branch. Pure and synchronous: BAPI-1009 removed the interactive proposal, so
|
|
975
|
+
* this asks nothing and reads no I/O.
|
|
976
|
+
*
|
|
977
|
+
* Precedence, in order:
|
|
818
978
|
*
|
|
819
|
-
*
|
|
820
|
-
*
|
|
821
|
-
*
|
|
822
|
-
*
|
|
823
|
-
*
|
|
824
|
-
*
|
|
825
|
-
*
|
|
826
|
-
*
|
|
827
|
-
*
|
|
828
|
-
*
|
|
829
|
-
*
|
|
830
|
-
*
|
|
979
|
+
* 1. `--into-base` returns `undefined` unconditionally. It is the explicit
|
|
980
|
+
* opt-out, and it wins over the derived default for every node count.
|
|
981
|
+
* 2. An explicit `--feature-branch <name>` is already confirmed + validated at
|
|
982
|
+
* parse time and is returned unchanged. It also forces an epic branch for a
|
|
983
|
+
* SINGLE-node plan, which rule 4 would otherwise leave on base.
|
|
984
|
+
* 3. A `--policy-file` that already declares a branch has ALREADY answered this
|
|
985
|
+
* question (BAPI-754), so no CLI branch is added — composition would then
|
|
986
|
+
* have to arbitrate between two answers.
|
|
987
|
+
* 4. A plan with fewer than {@link EPIC_BRANCH_MIN_PLAN_NODES} nodes returns
|
|
988
|
+
* `undefined` (R-4).
|
|
989
|
+
* 5. Otherwise the deterministic {@link proposeFeatureBranchName} value.
|
|
990
|
+
*
|
|
991
|
+
* BAPI-1009 deliberately removed the TTY/`--json` early return that used to
|
|
992
|
+
* produce `undefined` here. Branch strategy that depended on whether stdout was
|
|
993
|
+
* a terminal meant a CI run and an operator's run of the SAME command bootstrapped
|
|
994
|
+
* two different topologies — and the CI one was the one the shadow-index bootstrap
|
|
995
|
+
* was never exercised against.
|
|
831
996
|
*/
|
|
832
|
-
|
|
997
|
+
export function resolveFeatureBranchSelection(opts, planNodeCount, policyDeclaresBranch = false) {
|
|
998
|
+
if (opts.intoBase)
|
|
999
|
+
return undefined;
|
|
833
1000
|
if (opts.featureBranch !== undefined)
|
|
834
1001
|
return opts.featureBranch;
|
|
835
1002
|
if (policyDeclaresBranch)
|
|
836
1003
|
return undefined;
|
|
837
|
-
if (
|
|
1004
|
+
if (planNodeCount < EPIC_BRANCH_MIN_PLAN_NODES)
|
|
838
1005
|
return undefined;
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
deps.errorLog(` Invalid branch name: ${reason} Try again, or press Enter for the base branch.`);
|
|
858
|
-
continue; // redisplay for correction; never silently rewrite the value
|
|
859
|
-
}
|
|
860
|
-
return answer;
|
|
1006
|
+
return proposeFeatureBranchName(opts.epicKey);
|
|
1007
|
+
}
|
|
1008
|
+
/**
|
|
1009
|
+
* One line naming the selected branch strategy AND why it was selected.
|
|
1010
|
+
*
|
|
1011
|
+
* BAPI-1009: the "why" is the load-bearing half. The default changed, so an
|
|
1012
|
+
* operator reading `epic/BAPI-1009` needs to know whether they asked for it,
|
|
1013
|
+
* their policy file asked for it, or the multi-node default derived it — those
|
|
1014
|
+
* three have different recovery steps when the name is not the one they wanted.
|
|
1015
|
+
*/
|
|
1016
|
+
function describeBranchStrategy(opts, featureBranch, policyDeclaresBranch) {
|
|
1017
|
+
const cutNote = "cut on origin with local git at the canonical indexed commit; " +
|
|
1018
|
+
"validated server-side at approval";
|
|
1019
|
+
if (featureBranch !== undefined) {
|
|
1020
|
+
if (opts.featureBranch !== undefined) {
|
|
1021
|
+
return `${featureBranch} (explicit --feature-branch override; ${cutNote})`;
|
|
1022
|
+
}
|
|
1023
|
+
return `${featureBranch} (derived epic branch for this multi-ticket plan; ${cutNote})`;
|
|
861
1024
|
}
|
|
1025
|
+
if (opts.intoBase) {
|
|
1026
|
+
return "repository base branch — epic branch disabled by --into-base; every child PR targets base";
|
|
1027
|
+
}
|
|
1028
|
+
if (policyDeclaresBranch) {
|
|
1029
|
+
return "from the policy file's base_branch (the file already answered this)";
|
|
1030
|
+
}
|
|
1031
|
+
return (`repository base branch — a single-node plan keeps base, matching start-tickets ` +
|
|
1032
|
+
`(pass --feature-branch to force ${proposeFeatureBranchName(opts.epicKey)} anyway)`);
|
|
862
1033
|
}
|
|
863
1034
|
function errorDetail(err) {
|
|
864
1035
|
if (err instanceof ConductorBridgeApiError) {
|
|
@@ -939,6 +1110,36 @@ function emitRefusal(deps, opts, result) {
|
|
|
939
1110
|
* the newer commit.
|
|
940
1111
|
*/
|
|
941
1112
|
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)";
|
|
1113
|
+
/**
|
|
1114
|
+
* The canonical one-line recovery for a scope collision (BAPI-1009).
|
|
1115
|
+
*
|
|
1116
|
+
* Repeated VERBATIM as the epic-run gate snapshot's shadow-hold recovery hint
|
|
1117
|
+
* (``api/library/epic_conductor/gate_snapshot_service.py``), so an operator who
|
|
1118
|
+
* reads the diagnosis on the status surface and an operator who reads it from
|
|
1119
|
+
* the CLI are told to do the same thing in the same words.
|
|
1120
|
+
*/
|
|
1121
|
+
export const SETUP_EPIC_SCOPE_REDRIVE_RECOVERY = "Re-drive the cut at the newer commit.";
|
|
1122
|
+
/**
|
|
1123
|
+
* How long `setup-epic` waits for a scope cut before acknowledging it as
|
|
1124
|
+
* DEFERRED (BAPI-1054).
|
|
1125
|
+
*
|
|
1126
|
+
* Six polls at the shared 5-second interval — roughly 30 seconds. It is a fixed
|
|
1127
|
+
* value, injectable for tests but deliberately NOT operator-configurable, so
|
|
1128
|
+
* "returns promptly" is one testable number rather than a range that depends on
|
|
1129
|
+
* how the operator invoked the command.
|
|
1130
|
+
*
|
|
1131
|
+
* It bounds THIS caller only. {@link SCOPE_BOOTSTRAP_MAX_POLLS} is untouched, so
|
|
1132
|
+
* anything else that drives the shared poll keeps the generous default a real
|
|
1133
|
+
* seed needs. Waiting the full ~20 minutes was never buying information: the run
|
|
1134
|
+
* is already created and approved, the reconciler holds dispatch until the scope
|
|
1135
|
+
* is ready either way, and the only thing the long block changed was whether the
|
|
1136
|
+
* operator's terminal was occupied while it happened.
|
|
1137
|
+
*/
|
|
1138
|
+
export const SETUP_EPIC_SCOPE_DEFERRED_MAX_POLLS = 6;
|
|
1139
|
+
/** The deferred window in seconds, derived so prose cannot drift from the bound. */
|
|
1140
|
+
export const SETUP_EPIC_SCOPE_DEFERRED_WINDOW_SECONDS = Math.round((SETUP_EPIC_SCOPE_DEFERRED_MAX_POLLS * SCOPE_BOOTSTRAP_POLL_INTERVAL_MS) / 1000);
|
|
1141
|
+
/** The exact phrase the deferred acknowledgement leads with. Named so tests pin it. */
|
|
1142
|
+
export const SETUP_EPIC_CUT_DEFERRED_PHRASE = "created, cut deferred";
|
|
942
1143
|
/** The lead sentence of every fail-closed canonical-parse refusal (BAPI-850). */
|
|
943
1144
|
export const SETUP_EPIC_PARSE_FIRST_MESSAGE = "parse the repository first";
|
|
944
1145
|
/**
|
|
@@ -1047,9 +1248,20 @@ async function resolveScopeIdForRun(access, fetchImpl, epicRunId) {
|
|
|
1047
1248
|
* ONE run), so it is always named explicitly rather than left as a placeholder.
|
|
1048
1249
|
*/
|
|
1049
1250
|
function scopeFailureGuidance(reason, featureBranch, epicRunId) {
|
|
1251
|
+
// BAPI-1009: both collision categories lead with the SAME canonical sentence the
|
|
1252
|
+
// run snapshot's recovery hint repeats, then keep their own remediation detail.
|
|
1253
|
+
// The default now cuts a branch where it previously did not, so a collision is
|
|
1254
|
+
// reachable by operators who never chose a branch strategy at all — the recovery
|
|
1255
|
+
// has to read identically wherever they happen to encounter it.
|
|
1050
1256
|
if (reason === "canonical_index_advanced") {
|
|
1051
|
-
return (
|
|
1052
|
-
`
|
|
1257
|
+
return (`${SETUP_EPIC_SCOPE_REDRIVE_RECOVERY} The canonical index advanced before the ` +
|
|
1258
|
+
`scope could be seeded: delete origin/${featureBranch}, abandon this run ` +
|
|
1259
|
+
`(conductor abandon-run --epic-run-id ${epicRunId}), and re-run setup-epic.`);
|
|
1260
|
+
}
|
|
1261
|
+
if (reason === "head_mismatch") {
|
|
1262
|
+
return (`${SETUP_EPIC_SCOPE_REDRIVE_RECOVERY} origin/${featureBranch} does not sit at the ` +
|
|
1263
|
+
`commit this scope was cut at, and setup-epic never force-updates an existing ` +
|
|
1264
|
+
`ref: delete origin/${featureBranch}, abandon this run ` +
|
|
1053
1265
|
`(conductor abandon-run --epic-run-id ${epicRunId}), and re-run setup-epic.`);
|
|
1054
1266
|
}
|
|
1055
1267
|
return `Recovery: ${SETUP_EPIC_SCOPE_RECOVERY_COMMAND}.`;
|
|
@@ -1062,11 +1274,20 @@ function scopeFailureGuidance(reason, featureBranch, epicRunId) {
|
|
|
1062
1274
|
* detail: the label leads, the scope id never does, and no credential appears.
|
|
1063
1275
|
*/
|
|
1064
1276
|
function describeScopeTransition(state, status) {
|
|
1065
|
-
|
|
1277
|
+
// BAPI-1054: bounded on the way in, so an unrecognized server state renders as
|
|
1278
|
+
// `unknown` rather than as itself. `SCOPE_LIFECYCLE_LABELS[state] ?? state`
|
|
1279
|
+
// fell through to the raw value for anything outside the known set.
|
|
1280
|
+
const bounded = boundScopeLifecycleLabel(state);
|
|
1281
|
+
const label = SCOPE_LIFECYCLE_LABELS[bounded] ?? bounded;
|
|
1066
1282
|
if (state === "ready") {
|
|
1067
1283
|
return `Scope: ${label} — indexed at ${status.indexed_commit_sha ?? "unknown"} (scope ${status.scope_id})`;
|
|
1068
1284
|
}
|
|
1069
1285
|
if (state === "failed") {
|
|
1286
|
+
// `last_error` is the bounded failure CATEGORY, not free text: the terminal
|
|
1287
|
+
// branch already surfaces the same value as `index_scope.failure_reason` and
|
|
1288
|
+
// switches on it in `scopeFailureGuidance`. Dropping it here would lose the
|
|
1289
|
+
// one fact that distinguishes a recoverable collision from a seed failure,
|
|
1290
|
+
// while removing nothing that is not already reported.
|
|
1070
1291
|
return `Scope: ${label} — ${status.last_error ?? "unknown"} (scope ${status.scope_id})`;
|
|
1071
1292
|
}
|
|
1072
1293
|
return `Scope: ${label}…`;
|
|
@@ -1133,22 +1354,47 @@ export async function runSetupEpicCli(argv, overrides = {}) {
|
|
|
1133
1354
|
}
|
|
1134
1355
|
const policyDeclaresBranch = filePolicy !== undefined &&
|
|
1135
1356
|
["base_branch", "baseBranch"].some((key) => key in filePolicy && filePolicy[key] !== undefined && filePolicy[key] !== null);
|
|
1357
|
+
// --- Feature branch selection (BAPI-655, defaulted by BAPI-1009) ---------
|
|
1358
|
+
// Resolved from the VALIDATED plan's node count, the flags, and the policy
|
|
1359
|
+
// file — all of which are already in hand — and deliberately BEFORE the
|
|
1360
|
+
// `--local-only` return and before credential resolution. Selection reads no
|
|
1361
|
+
// network and asks no question, so running it here is what makes `--local-only`
|
|
1362
|
+
// and an ordinary invocation report the same strategy for the same plan.
|
|
1363
|
+
const featureBranch = resolveFeatureBranchSelection(opts, plan.nodes.length, policyDeclaresBranch);
|
|
1364
|
+
const branchStrategyLine = describeBranchStrategy(opts, featureBranch, policyDeclaresBranch);
|
|
1365
|
+
// --- Consolidated policy preflight (BAPI-1054) ---------------------------
|
|
1366
|
+
// ONE gate, run BEFORE credential resolution and before any request, over the
|
|
1367
|
+
// whole flag-backed choice-set. It sits above the `--local-only` return
|
|
1368
|
+
// deliberately: a dry run that refuses a different set from the real run is
|
|
1369
|
+
// worth very little, and this branch previously composed the policy without
|
|
1370
|
+
// evaluating the `--into-base` contradiction alongside it.
|
|
1371
|
+
//
|
|
1372
|
+
// The server's `RunPolicy` schema stays authoritative. This validates the
|
|
1373
|
+
// choices the CLI itself surfaces and nothing else — a field-named `422` is
|
|
1374
|
+
// still the better error for everything inside the policy file.
|
|
1375
|
+
const policyGate = preflightSetupEpicPolicy({
|
|
1376
|
+
intoBase: opts.intoBase,
|
|
1377
|
+
policyFile: opts.policyFile,
|
|
1378
|
+
filePolicy,
|
|
1379
|
+
policyDeclaresBranch,
|
|
1380
|
+
featureBranch,
|
|
1381
|
+
reviewPolicy: opts.reviewPolicy,
|
|
1382
|
+
});
|
|
1383
|
+
if (!policyGate.ok) {
|
|
1384
|
+
deps.errorLog(policyGate.message);
|
|
1385
|
+
return 1;
|
|
1386
|
+
}
|
|
1136
1387
|
// --- --local-only --dry-run: stop here, offline and explicitly partial ----
|
|
1137
1388
|
// Nothing below this point is reachable without the network, and the whole
|
|
1138
1389
|
// point of the flag is to not touch it — not even to resolve a credential.
|
|
1139
1390
|
if (opts.localOnly) {
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
featureBranch: opts.featureBranch,
|
|
1143
|
-
reviewPolicy: opts.reviewPolicy,
|
|
1144
|
-
});
|
|
1145
|
-
if (!composed.ok) {
|
|
1146
|
-
deps.errorLog(composed.error);
|
|
1147
|
-
return 1;
|
|
1148
|
-
}
|
|
1149
|
-
}
|
|
1391
|
+
// The policy gate above already composed and validated; nothing is repeated
|
|
1392
|
+
// here, so the offline path and the real path refuse the same set.
|
|
1150
1393
|
say(`Epic: ${opts.epicKey}`);
|
|
1151
1394
|
say(`Plan: v${plan.plan_version}, ${plan.nodes.length} node(s), ${plan.edges.length} edge(s)`);
|
|
1395
|
+
// BAPI-1009: the same node-count-derived strategy an ordinary run would take,
|
|
1396
|
+
// stated without cutting a branch, reading the network, or touching git.
|
|
1397
|
+
say(`Branch: ${branchStrategyLine}`);
|
|
1152
1398
|
say(`Local hash: ${localHash}`);
|
|
1153
1399
|
for (const w of warnings)
|
|
1154
1400
|
say(` [warn] ${w}`);
|
|
@@ -1193,35 +1439,19 @@ export async function runSetupEpicCli(argv, overrides = {}) {
|
|
|
1193
1439
|
say(`Local hash: ${localHash}`);
|
|
1194
1440
|
for (const w of warnings)
|
|
1195
1441
|
say(` [warn] ${w}`);
|
|
1196
|
-
// ---
|
|
1197
|
-
//
|
|
1198
|
-
//
|
|
1199
|
-
//
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
}
|
|
1210
|
-
// --- Compose the requested run policy (BAPI-754) -------------------------
|
|
1211
|
-
// A contradiction between the file and the flags is refused here, before the
|
|
1212
|
-
// pre-check and long before any create, so no request is dispatched under a
|
|
1213
|
-
// policy nobody chose.
|
|
1214
|
-
let requestedPolicy;
|
|
1215
|
-
if (filePolicy !== undefined) {
|
|
1216
|
-
const composed = composeSetupEpicPolicy(filePolicy, {
|
|
1217
|
-
featureBranch,
|
|
1218
|
-
reviewPolicy: opts.reviewPolicy,
|
|
1219
|
-
});
|
|
1220
|
-
if (!composed.ok) {
|
|
1221
|
-
deps.errorLog(composed.error);
|
|
1222
|
-
return 1;
|
|
1223
|
-
}
|
|
1224
|
-
requestedPolicy = composed.policy;
|
|
1442
|
+
// --- Branch strategy (BAPI-1009) ----------------------------------------
|
|
1443
|
+
// Already resolved above, before the network was touched. Reported HERE, ahead
|
|
1444
|
+
// of any secondary policy detail, because it is the one decision that changes
|
|
1445
|
+
// where every child PR lands — an operator who reads nothing else must still
|
|
1446
|
+
// see which topology this run is about to bootstrap.
|
|
1447
|
+
say(`Branch: ${branchStrategyLine}`);
|
|
1448
|
+
// --- The requested run policy (BAPI-754; gated by BAPI-1054) -------------
|
|
1449
|
+
// Composition and every contradiction refusal already happened in the policy
|
|
1450
|
+
// preflight above, before credentials were resolved. This is the SAME object
|
|
1451
|
+
// that passed the gate — not a second composition — so the payload the create
|
|
1452
|
+
// request carries is exactly what was validated.
|
|
1453
|
+
const requestedPolicy = policyGate.policy;
|
|
1454
|
+
if (requestedPolicy !== undefined) {
|
|
1225
1455
|
say(`Policy: ${opts.policyFile} (complete run policy, applied at creation)`);
|
|
1226
1456
|
}
|
|
1227
1457
|
// --- Effective feature branch (BAPI-850) ----------------------------------
|
|
@@ -1289,16 +1519,55 @@ export async function runSetupEpicCli(argv, overrides = {}) {
|
|
|
1289
1519
|
// --- Feature-branch conflict guard against an existing live run ----------
|
|
1290
1520
|
// A selected branch that disagrees with the existing run's stored base branch
|
|
1291
1521
|
// must fail closed rather than patch or silently change that run. A matching
|
|
1292
|
-
// branch is allowed (the rerun re-validates remote provisioning at approval)
|
|
1293
|
-
//
|
|
1522
|
+
// branch is allowed (the rerun re-validates remote provisioning at approval).
|
|
1523
|
+
//
|
|
1524
|
+
// BAPI-1009: `featureBranch` can now be DERIVED, so this guard also catches the
|
|
1525
|
+
// new case — a live run created under the old direct-to-base default, re-run
|
|
1526
|
+
// after the default flipped. Continuing would dispatch children at base while
|
|
1527
|
+
// every message on screen named an epic branch. The remedy differs by how the
|
|
1528
|
+
// branch was chosen, so the guidance names the flag that actually applies:
|
|
1529
|
+
// dropping `--feature-branch` reuses the run only when the flag is what asked
|
|
1530
|
+
// for the branch; when the DEFAULT asked, `--into-base` is the way to say
|
|
1531
|
+
// "keep this run on base".
|
|
1294
1532
|
if (existingRunId && featureBranch !== undefined && existingBaseBranch !== featureBranch) {
|
|
1533
|
+
const reuseAdvice = opts.featureBranch !== undefined
|
|
1534
|
+
? "Re-run without --feature-branch to reuse it unchanged"
|
|
1535
|
+
: `Re-run with --into-base to reuse it on the base branch it was created with`;
|
|
1295
1536
|
deps.errorLog(`Epic ${opts.epicKey} already has a live run (${existingRunId}) whose feature ` +
|
|
1296
1537
|
`branch is ${existingBaseBranch ? `'${existingBaseBranch}'` : "unset (base branch)"}, ` +
|
|
1297
|
-
`which conflicts with the
|
|
1298
|
-
`retarget or rebuild an existing run.
|
|
1299
|
-
`
|
|
1538
|
+
`which conflicts with the selected '${featureBranch}'. setup-epic will not ` +
|
|
1539
|
+
`retarget or rebuild an existing run. ${reuseAdvice}, or abandon the run to ` +
|
|
1540
|
+
`start over on a new branch.`);
|
|
1300
1541
|
return 1;
|
|
1301
1542
|
}
|
|
1543
|
+
// BAPI-1009: the mirror image — `--into-base` against a run that already lives
|
|
1544
|
+
// on an epic branch. The guard above cannot see this one, because `--into-base`
|
|
1545
|
+
// resolves to NO selected branch and so never disagrees with a stored value.
|
|
1546
|
+
// Reusing silently would report base mode for a run whose scope, cut, and
|
|
1547
|
+
// integration PR all belong to the epic branch.
|
|
1548
|
+
//
|
|
1549
|
+
// A stored `base_branch` is NOT by itself an epic branch: a run created with
|
|
1550
|
+
// `base_branch: "main"` is ALREADY on base, and refusing it would be a false
|
|
1551
|
+
// refusal. So the comparison is made against the repository's configured base,
|
|
1552
|
+
// resolved exactly the way the cut path below resolves it — same client, same
|
|
1553
|
+
// `"main"` fallback for an unreadable config — because "a branch equal to the
|
|
1554
|
+
// configured base is not scope-bearing" is an existing rule here, and a second
|
|
1555
|
+
// rule that disagreed with it would be worse than the extra read.
|
|
1556
|
+
if (existingRunId && opts.intoBase && existingBaseBranch !== null) {
|
|
1557
|
+
const configuredBase = await getConfigFieldBaseBranch(access, deps.fetch);
|
|
1558
|
+
const repositoryBase = configuredBase.ok && configuredBase.value.base_branch !== null
|
|
1559
|
+
? configuredBase.value.base_branch
|
|
1560
|
+
: "main";
|
|
1561
|
+
if (existingBaseBranch !== repositoryBase) {
|
|
1562
|
+
deps.errorLog(`Epic ${opts.epicKey} already has a live run (${existingRunId}) on feature branch ` +
|
|
1563
|
+
`'${existingBaseBranch}', not the repository base '${repositoryBase}', and ` +
|
|
1564
|
+
`--into-base asks for the repository base branch. setup-epic will not retarget ` +
|
|
1565
|
+
`an existing run. Re-run without --into-base to reuse it unchanged, or abandon ` +
|
|
1566
|
+
`the run (conductor abandon-run --epic-run-id ${existingRunId}) and recreate ` +
|
|
1567
|
+
`it on base.`);
|
|
1568
|
+
return 1;
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1302
1571
|
// --- Review-policy conflict guard against an existing live run ----------
|
|
1303
1572
|
// BAPI-679, analogous to the feature-branch guard above. An explicitly selected
|
|
1304
1573
|
// policy that disagrees with a live run's effective policy must fail closed:
|
|
@@ -1323,12 +1592,55 @@ export async function runSetupEpicCli(argv, overrides = {}) {
|
|
|
1323
1592
|
// row and an automation-start debit on the way to finding that out.
|
|
1324
1593
|
let validated = null;
|
|
1325
1594
|
let validateLaneMissing = false;
|
|
1595
|
+
// BAPI-1027 — the evidence the successful validation was given, carried onto
|
|
1596
|
+
// the later store call so both lanes judge the plan against the same facts.
|
|
1597
|
+
let trackedPathEvidence;
|
|
1326
1598
|
try {
|
|
1327
1599
|
validated = await validateEpicPlan(access, {
|
|
1328
1600
|
planVersion: plan.plan_version,
|
|
1329
1601
|
planBlob: plan,
|
|
1330
1602
|
epicKey: opts.epicKey,
|
|
1331
1603
|
}, deps.fetch);
|
|
1604
|
+
// BAPI-1027 — phase two of the two-phase exchange. The server names the
|
|
1605
|
+
// cited-document paths it could not resolve; only THEN does the CLI touch
|
|
1606
|
+
// git, and only for those exact paths. The rules stay server-side: the CLI
|
|
1607
|
+
// supplies facts and never decides whether a plan passes.
|
|
1608
|
+
//
|
|
1609
|
+
// Skipped entirely when the first response already carries blocking
|
|
1610
|
+
// rejections. Those are unrelated to trackedness, the plan is going to be
|
|
1611
|
+
// refused either way, and running git plus a second billable-free round trip
|
|
1612
|
+
// to enrich a verdict that will not change is pure latency.
|
|
1613
|
+
const unresolved = unresolvedCitedDocPaths(validated);
|
|
1614
|
+
if (validated.rejections.length === 0 && unresolved.length > 0) {
|
|
1615
|
+
const oracleRun = deps.runCommand;
|
|
1616
|
+
if (!oracleRun) {
|
|
1617
|
+
deps.errorLog(`Validation could not be completed: ${unresolved.length} cited document ` +
|
|
1618
|
+
`path(s) need a git trackedness check, but no local command runner is ` +
|
|
1619
|
+
`available in this environment. No run was created and no ` +
|
|
1620
|
+
`automation-start charge occurred.`);
|
|
1621
|
+
return 1;
|
|
1622
|
+
}
|
|
1623
|
+
const resolution = await resolveTrackedPaths(deps.cwd, unresolved, {
|
|
1624
|
+
runCommand: oracleRun,
|
|
1625
|
+
});
|
|
1626
|
+
if (!resolution.ok) {
|
|
1627
|
+
// An unresolved oracle is "validation could not be completed", NEVER
|
|
1628
|
+
// "these documents are missing". Continuing would store a plan whose
|
|
1629
|
+
// citations were never checked while reporting a clean preflight.
|
|
1630
|
+
deps.errorLog(`Validation could not be completed: the cited documents in this plan ` +
|
|
1631
|
+
`could not be checked against version control.\n${resolution.error}\n` +
|
|
1632
|
+
`No run was created and no automation-start charge occurred. Re-run ` +
|
|
1633
|
+
`from inside the repository working tree once git is available.`);
|
|
1634
|
+
return 1;
|
|
1635
|
+
}
|
|
1636
|
+
trackedPathEvidence = resolution.facts;
|
|
1637
|
+
validated = await validateEpicPlan(access, {
|
|
1638
|
+
planVersion: plan.plan_version,
|
|
1639
|
+
planBlob: plan,
|
|
1640
|
+
epicKey: opts.epicKey,
|
|
1641
|
+
trackedPaths: trackedPathEvidence,
|
|
1642
|
+
}, deps.fetch);
|
|
1643
|
+
}
|
|
1332
1644
|
}
|
|
1333
1645
|
catch (err) {
|
|
1334
1646
|
const status = err instanceof ConductorBridgeApiError ? err.status : undefined;
|
|
@@ -1351,16 +1663,36 @@ export async function runSetupEpicCli(argv, overrides = {}) {
|
|
|
1351
1663
|
`occurred.\nDetail: ${errorDetail(err)}`);
|
|
1352
1664
|
return 1;
|
|
1353
1665
|
}
|
|
1666
|
+
else if (isRetryableConflict(err)) {
|
|
1667
|
+
// BAPI-1027 — a RETRYABLE stale-CAS conflict (post-BAPI-1016). The plan is
|
|
1668
|
+
// fine and so is the epic; someone else wrote concurrently between the read
|
|
1669
|
+
// and this call. Rendered with retry language and deliberately WITHOUT any
|
|
1670
|
+
// close/adopt/abandon wording: telling an operator to abandon a run over a
|
|
1671
|
+
// conflict they should simply retry is the exact confusion the distinct
|
|
1672
|
+
// codes exist to prevent.
|
|
1673
|
+
deps.errorLog(`Plan changed concurrently; re-read and retry validation.\n` +
|
|
1674
|
+
`Nothing was created, stored, or charged — this is a transient ` +
|
|
1675
|
+
`concurrency conflict, not a problem with your plan or your epic.\n` +
|
|
1676
|
+
`Detail: ${errorDetail(err)}`);
|
|
1677
|
+
return 1;
|
|
1678
|
+
}
|
|
1354
1679
|
else if (status === 409) {
|
|
1355
1680
|
// The epic is WEDGED (multiple active runs), which says nothing about the
|
|
1356
1681
|
// plan. Reporting this as "server validation: failed" would send the
|
|
1357
1682
|
// operator to fix a sidecar that is perfectly fine. The pre-check above
|
|
1358
1683
|
// normally catches this first; this branch covers the race where a
|
|
1359
1684
|
// duplicate run appears between the pre-check and the validate call.
|
|
1685
|
+
//
|
|
1686
|
+
// BAPI-1027 — TERMINAL, and said so plainly: the server now names this
|
|
1687
|
+
// `ACTIVE_EPIC_RUN_EXISTS`, distinct from the retryable conflict handled
|
|
1688
|
+
// above. Retrying never clears it. The branch still fires on a bare 409
|
|
1689
|
+
// from a server that predates the code, so an older deployment keeps the
|
|
1690
|
+
// behavior it always had.
|
|
1360
1691
|
deps.errorLog(`Epic ${opts.epicKey} has MULTIPLE active runs — the plan could not be ` +
|
|
1361
1692
|
`validated because the epic itself is wedged, and every plan call will ` +
|
|
1362
|
-
`keep failing. This is
|
|
1363
|
-
`
|
|
1693
|
+
`keep failing. This is TERMINAL: it will not clear on retry, and it is ` +
|
|
1694
|
+
`NOT a problem with your plan. Adopt the run you want to keep, or ` +
|
|
1695
|
+
`abandon the duplicate:\n` +
|
|
1364
1696
|
` conductor abandon-run --epic-run-id <duplicate_epic_run_id>\n` +
|
|
1365
1697
|
`No run was created and no automation-start charge occurred.\n` +
|
|
1366
1698
|
`Detail: ${errorDetail(err)}`);
|
|
@@ -1372,9 +1704,37 @@ export async function runSetupEpicCli(argv, overrides = {}) {
|
|
|
1372
1704
|
return 1;
|
|
1373
1705
|
}
|
|
1374
1706
|
}
|
|
1707
|
+
if (validated && validated.rejections.length > 0) {
|
|
1708
|
+
// BAPI-1027 — a lint rejection is the plan's own fault and is terminal here:
|
|
1709
|
+
// no run is created, nothing is stored, and no charge occurs. Rendered to
|
|
1710
|
+
// STDERR (never stdout, which under --json carries the single result object
|
|
1711
|
+
// and which in MCP server mode is the stdio transport), one compact line per
|
|
1712
|
+
// rejection so an operator can fix every fault in one pass instead of
|
|
1713
|
+
// rediscovering them one round trip at a time.
|
|
1714
|
+
deps.errorLog(`Plan needs attention: ${validated.rejections.length} lint rejection(s).`);
|
|
1715
|
+
for (const rejection of validated.rejections) {
|
|
1716
|
+
deps.errorLog(` ${rejection.reasonCode}${renderRejectionSubject(rejection)}: ${rejection.explanation}`);
|
|
1717
|
+
}
|
|
1718
|
+
deps.errorLog("The plan was NOT stored and no run was created; no automation-start charge " +
|
|
1719
|
+
"occurred. Fix the plan file and re-run — setup-epic never rewrites a plan, " +
|
|
1720
|
+
"because that would change its hash.");
|
|
1721
|
+
return 1;
|
|
1722
|
+
}
|
|
1375
1723
|
if (validated) {
|
|
1376
1724
|
say("Validation: server validation: passed");
|
|
1725
|
+
say(" Plan ready to store");
|
|
1726
|
+
say(" No lint rejections found");
|
|
1377
1727
|
say(` plan hash ${validated.planHash}`);
|
|
1728
|
+
// BAPI-1027 — "nothing was found" and "everything was checked" are different
|
|
1729
|
+
// statements, and reporting the first as the second is the false assurance
|
|
1730
|
+
// this lane exists to remove. An incomplete evaluation is a WARNING on
|
|
1731
|
+
// stderr: the plan is storable, and the operator is told what went unchecked.
|
|
1732
|
+
if (!validated.evaluationComplete) {
|
|
1733
|
+
for (const notice of validated.notices) {
|
|
1734
|
+
deps.errorLog(`WARNING: ${notice.noticeCode}: ${notice.explanation}` +
|
|
1735
|
+
(notice.paths.length > 0 ? `\n Unchecked: ${notice.paths.join(", ")}` : ""));
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1378
1738
|
// BAPI-848 — three distinct outcomes, never collapsed into success language.
|
|
1379
1739
|
// "0 edges inserted" used to read as proof the siblings could not conflict.
|
|
1380
1740
|
// It never was: serialization only sees paths BOTH nodes declared, and the
|
|
@@ -1449,6 +1809,10 @@ export async function runSetupEpicCli(argv, overrides = {}) {
|
|
|
1449
1809
|
say(needsCut && cutCommitSha !== null
|
|
1450
1810
|
? ` - 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
1811
|
: ` - feature branch: ${effectiveFeatureBranch} (validated server-side at approval; no request made in dry-run)`);
|
|
1812
|
+
if (needsCut && cutCommitSha !== null && scopeBaseBranch !== null) {
|
|
1813
|
+
say(` - would open a draft pull request ${effectiveFeatureBranch} → ${scopeBaseBranch} ` +
|
|
1814
|
+
"for conductor-ci / gate (BAPI-951; nothing opened in dry-run)");
|
|
1815
|
+
}
|
|
1452
1816
|
}
|
|
1453
1817
|
if (opts.reviewPolicy !== undefined) {
|
|
1454
1818
|
say(` - review policy: ${opts.reviewPolicy} (per-run policy_json.review_policy; sets no repository default — use \`install-bridge conductor --review-policy\` for that)`);
|
|
@@ -1616,7 +1980,17 @@ export async function runSetupEpicCli(argv, overrides = {}) {
|
|
|
1616
1980
|
else {
|
|
1617
1981
|
// The pre-check said there was no live run, but create found one — a
|
|
1618
1982
|
// concurrent setup won the race. Treat it exactly like the reuse path.
|
|
1619
|
-
|
|
1983
|
+
//
|
|
1984
|
+
// BAPI-1027 — this ADOPTION is the normal answer to "a second setup for
|
|
1985
|
+
// one epic", not an error: `create_epic_run` returns the existing
|
|
1986
|
+
// non-terminal run untouched, and the `uq_epic_runs_active_repo_epic_key`
|
|
1987
|
+
// index is the race backstop that keeps it so. Naming the adopted run and
|
|
1988
|
+
// its status, rather than reporting a bare reuse, is what lets an operator
|
|
1989
|
+
// decide between continuing on it and closing it.
|
|
1990
|
+
say(`Run: adopting existing active run ${run.epic_run_id} (status: ${run.status})`);
|
|
1991
|
+
say(` No second run was created — one epic has at most one active ` +
|
|
1992
|
+
`run. Continue on this one, or abandon it first with ` +
|
|
1993
|
+
`\`conductor abandon-run --epic-run-id ${run.epic_run_id}\` to start over.`);
|
|
1620
1994
|
if (requestedPolicy !== undefined) {
|
|
1621
1995
|
const outcome = await applyReusedRunPolicy({
|
|
1622
1996
|
access,
|
|
@@ -1636,6 +2010,18 @@ export async function runSetupEpicCli(argv, overrides = {}) {
|
|
|
1636
2010
|
catch (err) {
|
|
1637
2011
|
const status = err instanceof ConductorBridgeApiError ? err.status : undefined;
|
|
1638
2012
|
if (status === 422 && requestedPolicy !== undefined) {
|
|
2013
|
+
// BAPI-1054 investigated whether `errorDetail`'s `bodyPreview` could echo
|
|
2014
|
+
// policy content here — a run policy can carry `notify.webhook_url`, and
|
|
2015
|
+
// a FastAPI validation detail routinely repeats the submitted `input`.
|
|
2016
|
+
//
|
|
2017
|
+
// It cannot, and the guard is one layer down where it belongs:
|
|
2018
|
+
// `formatValidationDetailItem` (conductor/bridge-api-client.ts) reduces a
|
|
2019
|
+
// 422 detail to `<loc path>: <msg>` — field NAMES and the validator's own
|
|
2020
|
+
// message, with `input` never read and the transport-only `body` prefix
|
|
2021
|
+
// dropped — and `redactDiagnosticValues` then scrubs known secrets.
|
|
2022
|
+
// Re-sanitizing here would only destroy the field name that makes the
|
|
2023
|
+
// server's field-named 422 actionable. The property is pinned by the
|
|
2024
|
+
// regression test below rather than left to this comment.
|
|
1639
2025
|
deps.errorLog(`The run policy from '${opts.policyFile}' was rejected by the server.\n` +
|
|
1640
2026
|
`${errorDetail(err)}\n` +
|
|
1641
2027
|
`No run was created and no automation-start charge occurred. Fix the ` +
|
|
@@ -1679,7 +2065,22 @@ export async function runSetupEpicCli(argv, overrides = {}) {
|
|
|
1679
2065
|
// and replace a stale record on its own, and there is nothing running to
|
|
1680
2066
|
// hand this binding to yet.
|
|
1681
2067
|
}
|
|
1682
|
-
|
|
2068
|
+
else if (manifestRead.kind !== "missing") {
|
|
2069
|
+
// NOT normal, and previously silent (BAPI-1029). `missing` genuinely
|
|
2070
|
+
// means "no local plane" — remote runs, CI, any non-plane workflow — and
|
|
2071
|
+
// stays quiet. `malformed` and `unreadable` mean a manifest EXISTS and
|
|
2072
|
+
// this build could not validate it, which is exactly what happens when
|
|
2073
|
+
// it was written by a newer build. A plane may well be running, and the
|
|
2074
|
+
// binding it needed just did not happen; saying nothing left the
|
|
2075
|
+
// operator to discover that at `plane down` time.
|
|
2076
|
+
const msg = `A local plane manifest exists but could not be validated (${manifestRead.error}), so ` +
|
|
2077
|
+
`run ${result.epic_run_id} was NOT bound to it. If a plane is running, \`plane down\` ` +
|
|
2078
|
+
"will not stop this run automatically — use " +
|
|
2079
|
+
`\`conductor stop-run --epic-run-id ${result.epic_run_id}\`. A manifest written by a ` +
|
|
2080
|
+
"NEWER build must be wound down with that build's `plane down`.";
|
|
2081
|
+
warnings.push(msg);
|
|
2082
|
+
say(`Plane: [warn] ${msg}`);
|
|
2083
|
+
}
|
|
1683
2084
|
}
|
|
1684
2085
|
catch (err) {
|
|
1685
2086
|
// The binding capability itself must never fail setup-epic as a whole;
|
|
@@ -1724,6 +2125,32 @@ export async function runSetupEpicCli(argv, overrides = {}) {
|
|
|
1724
2125
|
say(cut.branchCreated
|
|
1725
2126
|
? `Branch: cut origin/${effectiveFeatureBranch} at ${cut.lease.cut_commit_sha} with local git (scope ${cutScopeId})`
|
|
1726
2127
|
: `Branch: origin/${effectiveFeatureBranch} confirmed at ${cut.lease.cut_commit_sha} (scope ${cutScopeId}, cut already recorded)`);
|
|
2128
|
+
// --- Draft epic-integration PR (BAPI-951) -------------------------------
|
|
2129
|
+
// Only when THIS invocation actually created the branch — a reused/already
|
|
2130
|
+
// -recorded cut has already had its chance (setup-epic re-runs are common,
|
|
2131
|
+
// and a fresh probe would just re-confirm what a prior run already ensured
|
|
2132
|
+
// or deferred). Non-fatal in every outcome: setup-epic's provisioning
|
|
2133
|
+
// workflow (plan storage, approval, scope polling) proceeds unchanged.
|
|
2134
|
+
if (cut.branchCreated && scopeBaseBranch !== null) {
|
|
2135
|
+
const ghRunner = deps.epicIntegrationGh ?? createProductionEpicIntegrationGhRunner();
|
|
2136
|
+
let outcome;
|
|
2137
|
+
try {
|
|
2138
|
+
outcome = await ensureEpicIntegrationPullRequest({
|
|
2139
|
+
epicKey: opts.epicKey,
|
|
2140
|
+
epicBranch: effectiveFeatureBranch,
|
|
2141
|
+
baseBranch: scopeBaseBranch,
|
|
2142
|
+
command: "setup-epic",
|
|
2143
|
+
gh: ghRunner,
|
|
2144
|
+
cwd: deps.cwd,
|
|
2145
|
+
});
|
|
2146
|
+
}
|
|
2147
|
+
catch {
|
|
2148
|
+
outcome = { kind: "unavailable", reason: "probe_inconclusive" };
|
|
2149
|
+
}
|
|
2150
|
+
const formatted = formatEpicIntegrationPullRequestOutcome(outcome);
|
|
2151
|
+
result.epic_integration_pull_request = formatted;
|
|
2152
|
+
deps.errorLog(`Integration PR: ${JSON.stringify(formatted)}`);
|
|
2153
|
+
}
|
|
1727
2154
|
}
|
|
1728
2155
|
// --- Step 2/3: store the plan -------------------------------------------
|
|
1729
2156
|
try {
|
|
@@ -1732,6 +2159,12 @@ export async function runSetupEpicCli(argv, overrides = {}) {
|
|
|
1732
2159
|
planVersion: plan.plan_version,
|
|
1733
2160
|
planBlob: plan,
|
|
1734
2161
|
planHash: localHash,
|
|
2162
|
+
// BAPI-1027 — the same working-tree facts the dry run was validated
|
|
2163
|
+
// against. Without them the store lane would re-evaluate this plan with
|
|
2164
|
+
// NO evidence, report cited-document reachability as "not evaluated",
|
|
2165
|
+
// and quietly reach a different verdict than the preflight the operator
|
|
2166
|
+
// was just shown.
|
|
2167
|
+
...(trackedPathEvidence !== undefined ? { trackedPaths: trackedPathEvidence } : {}),
|
|
1735
2168
|
}, deps.fetch);
|
|
1736
2169
|
result.plan_stored = true;
|
|
1737
2170
|
const serverHash = stored?.plan_hash;
|
|
@@ -1838,8 +2271,14 @@ export async function runSetupEpicCli(argv, overrides = {}) {
|
|
|
1838
2271
|
exitCode = 1;
|
|
1839
2272
|
}
|
|
1840
2273
|
else {
|
|
1841
|
-
say(`Scope: waiting for index scope
|
|
1842
|
-
|
|
2274
|
+
say(`Scope: waiting up to ~${SETUP_EPIC_SCOPE_DEFERRED_WINDOW_SECONDS}s for index scope ` +
|
|
2275
|
+
`readiness; if it is still preparing, this returns and preparation continues server-side.`);
|
|
2276
|
+
const verdict = await pollIndexScopeLifecycle(cutProtocolDeps(deps, cutRunCommand ?? deps.runCommand ?? createExecFileRunCommand()), access, scopeId, {
|
|
2277
|
+
onTransition: (state, status) => say(describeScopeTransition(state, status)),
|
|
2278
|
+
// BAPI-1054: THIS caller's bound only. The shared default is untouched,
|
|
2279
|
+
// so nothing else that drives this poll waits any less than it did.
|
|
2280
|
+
maxPolls: SETUP_EPIC_SCOPE_DEFERRED_MAX_POLLS,
|
|
2281
|
+
});
|
|
1843
2282
|
if (verdict.kind === "ready") {
|
|
1844
2283
|
result.index_scope = {
|
|
1845
2284
|
scope_id: scopeId,
|
|
@@ -1884,20 +2323,25 @@ export async function runSetupEpicCli(argv, overrides = {}) {
|
|
|
1884
2323
|
exitCode = 1;
|
|
1885
2324
|
}
|
|
1886
2325
|
else {
|
|
2326
|
+
// BAPI-1054 (AC-2): NOT a failure. The run is created and approved, and
|
|
2327
|
+
// the reconciler holds dispatch until the scope is ready — a safe hold.
|
|
2328
|
+
// Reporting it as exit 1 after a ~20-minute block made a perfectly good
|
|
2329
|
+
// run read as a failed command, which is the whole defect this closes.
|
|
2330
|
+
//
|
|
2331
|
+
// Every rendered value is bounded: the run id, the scope id, and a
|
|
2332
|
+
// lifecycle label the poll already collapsed to a known label,
|
|
2333
|
+
// `unreadable`, or `unknown`. No raw status, no server error text, no URL.
|
|
2334
|
+
const lastState = boundScopeLifecycleLabel(verdict.lastState);
|
|
1887
2335
|
result.index_scope = {
|
|
1888
2336
|
scope_id: scopeId,
|
|
1889
2337
|
cut_commit_sha: verdict.lastStatus?.cut_commit_sha ?? cutCommitSha,
|
|
1890
|
-
lifecycle_state:
|
|
2338
|
+
lifecycle_state: lastState,
|
|
1891
2339
|
ready: false,
|
|
1892
|
-
disposition: "
|
|
2340
|
+
disposition: "created_cut_deferred",
|
|
1893
2341
|
};
|
|
1894
|
-
|
|
1895
|
-
`
|
|
1896
|
-
`
|
|
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;
|
|
2342
|
+
say(`Scope: ${SETUP_EPIC_CUT_DEFERRED_PHRASE} — run ${result.epic_run_id}, ` +
|
|
2343
|
+
`scope ${scopeId}, last observed state '${lastState}'. Scope preparation ` +
|
|
2344
|
+
`continues server-side; the reconciler holds dispatch until it is ready.`);
|
|
1901
2345
|
}
|
|
1902
2346
|
}
|
|
1903
2347
|
}
|
|
@@ -1915,7 +2359,16 @@ export async function runSetupEpicCli(argv, overrides = {}) {
|
|
|
1915
2359
|
"order was used. Check that BAPI_BASE_URL points at the intended deployment.");
|
|
1916
2360
|
}
|
|
1917
2361
|
say(`Epic run ${result.epic_run_id} is ${result.status ?? "unknown"}.`);
|
|
1918
|
-
|
|
2362
|
+
// BAPI-1054: a deferred cut means the reconciler picks the run up but HOLDS
|
|
2363
|
+
// dispatch, so the unqualified "~30s" line would contradict the
|
|
2364
|
+
// acknowledgement printed moments earlier.
|
|
2365
|
+
if (result.index_scope?.disposition === "created_cut_deferred") {
|
|
2366
|
+
say("The server-side reconciler will pick it up within ~30s, but holds ticket " +
|
|
2367
|
+
"dispatch until the index scope finishes preparing.");
|
|
2368
|
+
}
|
|
2369
|
+
else {
|
|
2370
|
+
say("The server-side reconciler will pick it up within ~30s.");
|
|
2371
|
+
}
|
|
1919
2372
|
say("To execute claimed jobs on this machine, run:");
|
|
1920
2373
|
say(` npx -y ${MCP_PACKAGE_NAME} executor --repo ${access.repoName}`);
|
|
1921
2374
|
}
|