@bridge_gpt/mcp-server 0.2.53 → 0.2.54
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 +86 -10
- package/build/agent-launchers/claude.js +3 -3
- package/build/agent-launchers/prompt.js +8 -11
- package/build/base-ref.js +33 -9
- package/build/bounded-wait.js +174 -0
- package/build/commands.generated.js +1 -1
- package/build/conductor/bridge-api-client.js +36 -8
- package/build/conductor/epic-runtime.js +133 -97
- package/build/conductor/readiness.js +85 -0
- package/build/conductor/run-branch.js +137 -0
- package/build/conductor/test-run-branch-vectors.js +165 -0
- package/build/conductor-bin.js +5 -5
- package/build/doctor.js +68 -1
- package/build/drive-epic.js +287 -51
- package/build/executor/claim-scope.js +104 -0
- package/build/executor/cli.js +14 -25
- package/build/executor/env-file-guard.js +82 -3
- package/build/executor/job-runner.js +60 -0
- package/build/index.js +128 -400
- package/build/local-artifact-storage.js +130 -0
- package/build/pipelines.generated.js +16 -9
- package/build/plane/cli.js +285 -36
- package/build/plane/manifest.js +209 -1
- package/build/plane/member-roster.js +70 -0
- package/build/plane/shutdown.js +14 -1
- package/build/plane/status.js +35 -1
- package/build/plane/supervisor.js +546 -164
- package/build/plane/types.js +25 -2
- package/build/polling-policy.js +72 -0
- package/build/readme.generated.js +1 -1
- package/build/review-generation.js +219 -0
- package/build/run-unit-tests-launcher.js +5 -0
- package/build/setup-epic.js +514 -23
- package/build/ticket-key-utils.js +4 -3
- package/build/ticket-review-artifact-gate.js +461 -0
- package/build/upgrade-cli.js +5 -26
- package/build/version.generated.js +3 -3
- package/docs/install/mcp-tool-integrations.md +23 -1
- package/package.json +1 -1
- package/pipelines/review-ticket.json +17 -4
package/build/setup-epic.js
CHANGED
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
import { readFile as fsReadFile, stat as fsStat } from "node:fs/promises";
|
|
79
79
|
import os from "node:os";
|
|
80
80
|
import { createProductionEpicIntegrationGhRunner, ensureEpicIntegrationPullRequest, formatEpicIntegrationPullRequestOutcome, } from "./epic-integration-pr.js";
|
|
81
|
-
import { approveEpicPlan, createEpicRunWithDisposition, fetchEpicRunState, replaceEpicRunPolicy, resolveConductorBridgeApiAccess, storeEpicPlan, validateEpicPlan, ConductorBridgeApiError, } from "./conductor/bridge-api-client.js";
|
|
81
|
+
import { approveEpicPlan, createEpicRunWithDisposition, fetchConductorReadiness, fetchEpicRunState, replaceEpicRunPolicy, resolveConductorBridgeApiAccess, storeEpicPlan, validateEpicPlan, ConductorBridgeApiError, } from "./conductor/bridge-api-client.js";
|
|
82
82
|
import { resolveTrackedPaths } from "./git-reachability.js";
|
|
83
83
|
import { validateBranchName } from "./base-ref.js";
|
|
84
84
|
// Reused from the conduct-epic client on purpose: adding a second wrapper under
|
|
@@ -98,6 +98,35 @@ import { MCP_PACKAGE_NAME } from "./mcp-identity.js";
|
|
|
98
98
|
// state. See `planeBinding` on `SetupEpicDeps` below.
|
|
99
99
|
import { createPlaneFsDeps, createPlaneProcessDeps } from "./plane/defaults.js";
|
|
100
100
|
import { bindPlaneManifestEpicRun, manifestHasLiveProcess, readPlaneManifest, } from "./plane/manifest.js";
|
|
101
|
+
/**
|
|
102
|
+
* The one `policy_json.posture` value that means "unattended" (BAPI-1102).
|
|
103
|
+
*
|
|
104
|
+
* Mirrors the server's `api.models.epic_run.RUN_POSTURE_UNATTENDED`, which is a
|
|
105
|
+
* closed one-member vocabulary: the model refuses any other value and treats an
|
|
106
|
+
* omitted key as attended. Written out here rather than derived, because the
|
|
107
|
+
* client cannot import a Python constant; a unit test pins the two together.
|
|
108
|
+
*/
|
|
109
|
+
export const SETUP_EPIC_UNATTENDED_POSTURE = "unattended";
|
|
110
|
+
/**
|
|
111
|
+
* The server's stable error code for an unattended-posture refusal (BAPI-734).
|
|
112
|
+
*
|
|
113
|
+
* Mirrors `api.library.epic_conductor.unattended_posture.UNATTENDED_POSTURE_REFUSED_CODE`.
|
|
114
|
+
* Matched by EQUALITY on the structured `error_code`, never by searching the
|
|
115
|
+
* message text: the message is prose that may be rewritten, the code is the
|
|
116
|
+
* contract.
|
|
117
|
+
*/
|
|
118
|
+
export const SETUP_EPIC_UNATTENDED_POSTURE_REFUSED_CODE = "UNATTENDED_POSTURE_REFUSED";
|
|
119
|
+
/**
|
|
120
|
+
* The `policy_json` key authorizing automatic merge dispatch (BAPI-722).
|
|
121
|
+
*
|
|
122
|
+
* Composed into the unattended default alongside the posture. The two are not
|
|
123
|
+
* redundant: `posture` selects the unattended predicate set and the escalation
|
|
124
|
+
* discipline, `v2_auto_merge_enabled` is what actually authorizes the reconciler
|
|
125
|
+
* to dispatch a merge. A run with the posture and without the flag is a valid,
|
|
126
|
+
* fully supported configuration — it simply parks at every merge for a human,
|
|
127
|
+
* which is what the observed BAPI-1085 run did.
|
|
128
|
+
*/
|
|
129
|
+
export const SETUP_EPIC_AUTO_MERGE_POLICY_KEY = "v2_auto_merge_enabled";
|
|
101
130
|
/** Accepted `policy_json.review_policy.source` values (the `ReviewPolicy` surface). */
|
|
102
131
|
export const SETUP_EPIC_REVIEW_POLICY_SOURCES = [
|
|
103
132
|
"verdict_protocol",
|
|
@@ -138,6 +167,19 @@ export function getSetupEpicUsage() {
|
|
|
138
167
|
"integration PR targets the repository base branch. A single-node plan keeps the",
|
|
139
168
|
"repository base branch, matching plain start-tickets semantics.",
|
|
140
169
|
"",
|
|
170
|
+
"Posture (default): a run that selects an epic branch is created UNATTENDED —",
|
|
171
|
+
"posture: unattended and v2_auto_merge_enabled: true — and the server stamps its",
|
|
172
|
+
"CI gate at first approval from the repository default, or records an explicit",
|
|
173
|
+
"no-CI-gate mode when the repository declares none. Child PRs merge into the epic",
|
|
174
|
+
"branch; the INTEGRATION PR into the repository base branch stays human-gated.",
|
|
175
|
+
"Pass --attended to opt out. A run that selects no epic branch (--into-base, or a",
|
|
176
|
+
"single-node plan) is unchanged and composes no posture at all.",
|
|
177
|
+
"",
|
|
178
|
+
"Unattended runs have TWO one-time repository prerequisites, refused by name",
|
|
179
|
+
"before any run is created: repository consent (unattended_conductor_allowed) and",
|
|
180
|
+
"a verified notify webhook default. `drive-epic` readiness lists both with their",
|
|
181
|
+
"current state.",
|
|
182
|
+
"",
|
|
141
183
|
"Required:",
|
|
142
184
|
" --epic-key <KEY> Jira epic key (e.g. BAPI-405)",
|
|
143
185
|
" --plan-file <path> Path to epic-plan.dag.json (from decompose-epic)",
|
|
@@ -165,6 +207,13 @@ export function getSetupEpicUsage() {
|
|
|
165
207
|
" (the pre-BAPI-1009 behavior). Use this if automation",
|
|
166
208
|
" depended on \"no flag = direct to base\". Contradictory",
|
|
167
209
|
" with --feature-branch; passing both is a parse error.",
|
|
210
|
+
" --attended OPT OUT of the unattended default: compose only",
|
|
211
|
+
" base_branch (plus --review-policy when supplied), with",
|
|
212
|
+
" no posture and no auto-merge authorization, and skip the",
|
|
213
|
+
" unattended repository prerequisites entirely. The create",
|
|
214
|
+
" request is byte-for-byte the pre-BAPI-1102 one.",
|
|
215
|
+
" Contradictory with a --policy-file that declares",
|
|
216
|
+
" posture: unattended; that pair is refused by name.",
|
|
168
217
|
" --review-policy <src> PER-RUN review policy source, one of:",
|
|
169
218
|
` ${SETUP_EPIC_REVIEW_POLICY_SOURCES.join(", ")}.`,
|
|
170
219
|
" Composed into policy_json.review_policy on create.",
|
|
@@ -269,6 +318,7 @@ export function parseSetupEpicArgs(argv) {
|
|
|
269
318
|
// fire on the INTENT, not on whether a name survived normalization.
|
|
270
319
|
let featureBranchSupplied = false;
|
|
271
320
|
let intoBase = false;
|
|
321
|
+
let attended = false;
|
|
272
322
|
let reviewPolicy;
|
|
273
323
|
let policyFile;
|
|
274
324
|
let replacePolicy = false;
|
|
@@ -379,6 +429,14 @@ export function parseSetupEpicArgs(argv) {
|
|
|
379
429
|
case "--into-base":
|
|
380
430
|
intoBase = true;
|
|
381
431
|
break;
|
|
432
|
+
// BAPI-1102. Valueless, like `--into-base` and `--replace-policy`. It is
|
|
433
|
+
// deliberately NOT contradictory with `--into-base`: a direct-to-main run
|
|
434
|
+
// composes no policy at all, so `--attended` is redundant there rather than
|
|
435
|
+
// conflicting, and refusing a redundant flag would be a worse experience
|
|
436
|
+
// than accepting it.
|
|
437
|
+
case "--attended":
|
|
438
|
+
attended = true;
|
|
439
|
+
break;
|
|
382
440
|
case "--replace-policy":
|
|
383
441
|
replacePolicy = true;
|
|
384
442
|
break;
|
|
@@ -442,6 +500,7 @@ export function parseSetupEpicArgs(argv) {
|
|
|
442
500
|
planVersion,
|
|
443
501
|
featureBranch,
|
|
444
502
|
intoBase,
|
|
503
|
+
attended,
|
|
445
504
|
reviewPolicy,
|
|
446
505
|
policyFile,
|
|
447
506
|
replacePolicy,
|
|
@@ -523,6 +582,15 @@ export function composeSetupEpicPolicy(filePolicy, selections) {
|
|
|
523
582
|
// contradiction purposes so a conflicting value is reported rather than
|
|
524
583
|
// silently ignored, but never invent it and never rewrite it into snake_case:
|
|
525
584
|
// the server's strict boundary should be the one to reject it.
|
|
585
|
+
//
|
|
586
|
+
// BAPI-1127: this paired read is CONTRADICTION DETECTION over a policy file
|
|
587
|
+
// the operator just handed in, at SETUP time, before any run exists — it is
|
|
588
|
+
// not run-branch resolution and must never become one. It asks "did the file
|
|
589
|
+
// name a branch under either spelling that disagrees with --feature-branch?",
|
|
590
|
+
// so it reads both keys for PRESENCE and deliberately picks no winner between
|
|
591
|
+
// them. `conductor/run-branch.ts#resolveDeclaredRunBaseBranch` is the single
|
|
592
|
+
// authority for resolving a PERSISTED run's declared branch; this is a
|
|
593
|
+
// documented exception to that, not a second implementation of it.
|
|
526
594
|
const declaredBranches = [];
|
|
527
595
|
for (const key of ["base_branch", "baseBranch"]) {
|
|
528
596
|
if (key in composed && composed[key] !== undefined && composed[key] !== null) {
|
|
@@ -611,7 +679,30 @@ export function preflightSetupEpicPolicy(args) {
|
|
|
611
679
|
`branch declaration from the policy file, or drop --into-base.`,
|
|
612
680
|
};
|
|
613
681
|
}
|
|
614
|
-
// 2.
|
|
682
|
+
// 2. Posture contradiction (BAPI-1102). Placed SECOND — after the branch
|
|
683
|
+
// strategy, before the composer's field-level conflicts — so one invocation
|
|
684
|
+
// still yields exactly one named reason, deterministically. It belongs above
|
|
685
|
+
// the composer because it is a whole-INTENT contradiction: an operator who
|
|
686
|
+
// said "attended" and supplied a file that says "unattended" has not made a
|
|
687
|
+
// field mistake, and reporting a downstream field conflict first would send
|
|
688
|
+
// them to the wrong line of the file.
|
|
689
|
+
//
|
|
690
|
+
// READ-ONLY, and the read is deliberately the narrowest possible: only
|
|
691
|
+
// whether the file explicitly declares `posture: "unattended"`. Nothing is
|
|
692
|
+
// normalized, no other key is inspected, and — like every message here — the
|
|
693
|
+
// refusal interpolates the file PATH and fixed prose only, never a value
|
|
694
|
+
// read out of the file.
|
|
695
|
+
if (args.attended === true && policyFileDeclaresUnattendedPosture(args.filePolicy)) {
|
|
696
|
+
return {
|
|
697
|
+
ok: false,
|
|
698
|
+
reason: "attended_with_policy_file_posture",
|
|
699
|
+
message: `--attended opts out of the unattended default, but --policy-file ` +
|
|
700
|
+
`'${args.policyFile}' declares posture: "unattended". setup-epic will not ` +
|
|
701
|
+
`pick a winner between them: drop --attended, or remove the posture ` +
|
|
702
|
+
`declaration from the policy file.`,
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
// 3. Policy-source contradictions between the file and the CLI selections.
|
|
615
706
|
// Delegated to the composer, which owns the "no silent winner" rule; the
|
|
616
707
|
// gate only gives the outcome a stable name.
|
|
617
708
|
if (args.filePolicy === undefined)
|
|
@@ -625,6 +716,24 @@ export function preflightSetupEpicPolicy(args) {
|
|
|
625
716
|
}
|
|
626
717
|
return { ok: true, policy: composed.policy };
|
|
627
718
|
}
|
|
719
|
+
/**
|
|
720
|
+
* Does this policy file explicitly declare the UNATTENDED posture (BAPI-1102)?
|
|
721
|
+
*
|
|
722
|
+
* Strict by construction, and narrow on purpose. `true` only for a plain object
|
|
723
|
+
* whose `posture` is exactly the string `"unattended"` — the one spelling the
|
|
724
|
+
* server's `RUN_POSTURE_UNATTENDED` accepts. It never reads another key, never
|
|
725
|
+
* normalizes, and never returns the value it read, so the caller cannot leak a
|
|
726
|
+
* policy value into a refusal message even by accident.
|
|
727
|
+
*
|
|
728
|
+
* A file that declares no posture is not a contradiction: `--attended` is the
|
|
729
|
+
* opt-out from a DEFAULT, and a file that stays silent about posture leaves that
|
|
730
|
+
* default in play for `--attended` to decline.
|
|
731
|
+
*/
|
|
732
|
+
export function policyFileDeclaresUnattendedPosture(filePolicy) {
|
|
733
|
+
if (filePolicy === undefined || filePolicy === null)
|
|
734
|
+
return false;
|
|
735
|
+
return filePolicy.posture === SETUP_EPIC_UNATTENDED_POSTURE;
|
|
736
|
+
}
|
|
628
737
|
/**
|
|
629
738
|
* Does this policy object explicitly supply the `notify.webhook_url` leaf?
|
|
630
739
|
*
|
|
@@ -1005,6 +1114,233 @@ export function resolveFeatureBranchSelection(opts, planNodeCount, policyDeclare
|
|
|
1005
1114
|
return undefined;
|
|
1006
1115
|
return proposeFeatureBranchName(opts.epicKey);
|
|
1007
1116
|
}
|
|
1117
|
+
/**
|
|
1118
|
+
* Evaluate the unattended prerequisites against a readiness report.
|
|
1119
|
+
*
|
|
1120
|
+
* Pure and total: it takes an already-read report (or `null` for a failed read)
|
|
1121
|
+
* and the COMPOSED policy the create request will carry, and returns at most ONE
|
|
1122
|
+
* refusal, in a fixed order that matches the server's own cheapest-first
|
|
1123
|
+
* sequence, so readiness, this gate, and the approval refusal all name the same
|
|
1124
|
+
* first blocker.
|
|
1125
|
+
*
|
|
1126
|
+
* The policy is a parameter rather than something re-derived here because one
|
|
1127
|
+
* prerequisite — the escalation channel — is satisfiable per RUN as well as per
|
|
1128
|
+
* repository. A version of this gate that read only the repository report
|
|
1129
|
+
* refused every unattended run, its own named remedy included.
|
|
1130
|
+
*
|
|
1131
|
+
* The reason CODES are the server's bounded vocabulary, spelled exactly as
|
|
1132
|
+
* `api/library/epic_conductor/unattended_posture.py` spells them. The client adds
|
|
1133
|
+
* remediation TEXT around them and invents no aliases — an alias would drift from
|
|
1134
|
+
* the server's refusal the first time either side was edited, and an operator
|
|
1135
|
+
* comparing a CLI message with an API response would have no way to tell they
|
|
1136
|
+
* were the same refusal.
|
|
1137
|
+
*
|
|
1138
|
+
* `readiness_unreadable` is the one code with no server twin, and it is
|
|
1139
|
+
* deliberately generic and secret-free: an unreadable or older-server report has
|
|
1140
|
+
* PROVEN nothing, and fail-closed is the only safe direction for a check that
|
|
1141
|
+
* authorizes unattended merging.
|
|
1142
|
+
*/
|
|
1143
|
+
/**
|
|
1144
|
+
* Does this policy declare a notify webhook the server would probe?
|
|
1145
|
+
*
|
|
1146
|
+
* BAPI-1102. The shape check mirrors `_extract_webhook_url` in
|
|
1147
|
+
* `api/library/epic_conductor/webhook_verification.py` exactly as far as
|
|
1148
|
+
* PRESENCE goes — a `notify` object carrying a non-blank string `webhook_url` —
|
|
1149
|
+
* and stops there on purpose. Scheme, host, and reachability are the server's to
|
|
1150
|
+
* judge at activation, where the probe actually happens; re-deciding them here
|
|
1151
|
+
* would mean a client that refused a URL the server accepts, or accepted one it
|
|
1152
|
+
* refuses, the first time either side's rules were edited.
|
|
1153
|
+
*
|
|
1154
|
+
* `notify.local_sink` deliberately does NOT satisfy this, matching the server: a
|
|
1155
|
+
* local sink has no destination to prove reachable, so it is not an escalation
|
|
1156
|
+
* channel for a run nobody is watching.
|
|
1157
|
+
*/
|
|
1158
|
+
export function policyDeclaresNotifyWebhook(policy) {
|
|
1159
|
+
if (policy === undefined || policy === null)
|
|
1160
|
+
return false;
|
|
1161
|
+
const notify = policy.notify;
|
|
1162
|
+
if (typeof notify !== "object" || notify === null || Array.isArray(notify)) {
|
|
1163
|
+
return false;
|
|
1164
|
+
}
|
|
1165
|
+
const url = notify.webhook_url;
|
|
1166
|
+
return typeof url === "string" && url.trim() !== "";
|
|
1167
|
+
}
|
|
1168
|
+
export function evaluateSetupEpicUnattendedPrerequisites(readiness, requestedPolicy) {
|
|
1169
|
+
const unattended = readiness?.unattended ?? null;
|
|
1170
|
+
if (unattended === null) {
|
|
1171
|
+
return {
|
|
1172
|
+
reason: "readiness_unreadable",
|
|
1173
|
+
message: "the Bridge API did not report this repository's unattended prerequisites, so " +
|
|
1174
|
+
"they could not be confirmed. Retry once the server is reachable and running a " +
|
|
1175
|
+
"build that reports them, or pass --attended to create an attended run. No run " +
|
|
1176
|
+
"was created.",
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
if (!unattended.conductor_allowed) {
|
|
1180
|
+
return {
|
|
1181
|
+
reason: "target_not_allowed",
|
|
1182
|
+
message: "this repository has not consented to unattended conductor runs. Set " +
|
|
1183
|
+
"`unattended_conductor_allowed` for it (see " +
|
|
1184
|
+
"docs/claude/account-settings-operator-runbook.md), or pass --attended to " +
|
|
1185
|
+
"create an attended run. No run was created.",
|
|
1186
|
+
};
|
|
1187
|
+
}
|
|
1188
|
+
// Two independent sources satisfy the escalation channel, and the check has to
|
|
1189
|
+
// consult BOTH or it refuses its own documented remedy. No column supplies a
|
|
1190
|
+
// repository-level notify default today, so `notify_webhook_default_declared`
|
|
1191
|
+
// is `false` for every repository; reading only that field made this branch
|
|
1192
|
+
// refuse EVERY unattended run — including the `--policy-file` supplying
|
|
1193
|
+
// `notify.webhook_url` that its own message names as the fix — and left
|
|
1194
|
+
// `--attended` as the only reachable path. The run-level declaration is the
|
|
1195
|
+
// supported form, so it is the one that has to be honoured here.
|
|
1196
|
+
//
|
|
1197
|
+
// Presence, not verification: the server probes the URL at activation and is
|
|
1198
|
+
// the authority on whether it is reachable. This gate exists to spare the
|
|
1199
|
+
// operator a created-and-refused run, not to duplicate the probe.
|
|
1200
|
+
if (!unattended.notify_webhook_default_declared &&
|
|
1201
|
+
!policyDeclaresNotifyWebhook(requestedPolicy)) {
|
|
1202
|
+
return {
|
|
1203
|
+
reason: "webhook_unverified",
|
|
1204
|
+
message: "an unattended run must have a verified escalation channel, and neither this " +
|
|
1205
|
+
"repository nor this run declares one. Supply a --policy-file declaring " +
|
|
1206
|
+
"notify.webhook_url (notify.local_sink does not satisfy this), or pass " +
|
|
1207
|
+
"--attended to create an attended run. No run was created.",
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1210
|
+
if (!unattended.repository_readiness_confirmed) {
|
|
1211
|
+
return {
|
|
1212
|
+
reason: "repository_readiness_unconfirmed",
|
|
1213
|
+
message: "the unattended default authorizes automatic merge, which requires the review and " +
|
|
1214
|
+
"conductor-CI workflows to be CONFIRMED on the default branch, and they are not. " +
|
|
1215
|
+
"Run `install-bridge conductor` to install them, or pass --attended to create an " +
|
|
1216
|
+
"attended run. No run was created.",
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
return null;
|
|
1220
|
+
}
|
|
1221
|
+
/**
|
|
1222
|
+
* Does the policy this invocation will actually send request unattended operation?
|
|
1223
|
+
*
|
|
1224
|
+
* Read off the COMPOSED policy — the same object the create request carries —
|
|
1225
|
+
* rather than off the flags, so a `--policy-file` that declares
|
|
1226
|
+
* `posture: "unattended"` is gated exactly as the default profile is. Reading the
|
|
1227
|
+
* flags instead would let the file route around the prerequisites, which is the
|
|
1228
|
+
* one thing this gate exists to prevent.
|
|
1229
|
+
*
|
|
1230
|
+
* `--attended` short-circuits before the read: it is the explicit opt-out, and
|
|
1231
|
+
* the preflight gate above has already refused the one case where it contradicts
|
|
1232
|
+
* a policy file.
|
|
1233
|
+
*/
|
|
1234
|
+
export function requestedPolicyIsUnattended(policy, attended) {
|
|
1235
|
+
if (attended)
|
|
1236
|
+
return false;
|
|
1237
|
+
if (policy === undefined)
|
|
1238
|
+
return false;
|
|
1239
|
+
return policy.posture === SETUP_EPIC_UNATTENDED_POSTURE;
|
|
1240
|
+
}
|
|
1241
|
+
/**
|
|
1242
|
+
* Compose the create-time `policy_json` for a run with NO `--policy-file`.
|
|
1243
|
+
*
|
|
1244
|
+
* BAPI-1102. Extracted from the create branch so the default profile has one
|
|
1245
|
+
* definition and one place to read it, rather than an inline object literal that
|
|
1246
|
+
* a later edit could silently widen.
|
|
1247
|
+
*
|
|
1248
|
+
* Three shapes, and the boundaries between them are the contract:
|
|
1249
|
+
*
|
|
1250
|
+
* * **No feature branch selected** → `{}`. `--into-base`, a single-node plan,
|
|
1251
|
+
* and the branch-silent path each keep the EXACT legacy create request shape:
|
|
1252
|
+
* the caller sends no `policy_json` key at all, byte-for-byte what it sent
|
|
1253
|
+
* before BAPI-1009 (AC-9). Adding a posture, an auto-merge authorization, an
|
|
1254
|
+
* empty policy object, or anything else here would break all three at once —
|
|
1255
|
+
* and, worse, would ask the server to admit an ungated auto-merge on a run
|
|
1256
|
+
* whose children merge straight into the repository base branch.
|
|
1257
|
+
* * **Feature branch, `--attended`** → `base_branch` plus an explicitly
|
|
1258
|
+
* requested `review_policy`, and nothing else. Byte-identical to the
|
|
1259
|
+
* pre-BAPI-1102 composition, which is precisely what the opt-out promises.
|
|
1260
|
+
* * **Feature branch, default** → adds `posture: "unattended"` and
|
|
1261
|
+
* `v2_auto_merge_enabled: true`.
|
|
1262
|
+
*
|
|
1263
|
+
* Deliberately absent from every shape:
|
|
1264
|
+
*
|
|
1265
|
+
* * **`caps`.** Absent and JSON `null` both mean "the system default applies"
|
|
1266
|
+
* (`unattended_posture.py`), so composing a caps object here would replace a
|
|
1267
|
+
* server-owned ceiling table with a client-authored one.
|
|
1268
|
+
* * **`required_checks` / `no_ci_gate`.** The client has no business encoding a
|
|
1269
|
+
* repository's check names, and a repository default may change between run
|
|
1270
|
+
* creation and approval. First approval resolves the CI gate server-side from
|
|
1271
|
+
* the repository default, or records the explicit no-gate mode — and the stamp
|
|
1272
|
+
* is fill-if-ABSENT, so anything composed here would suppress it.
|
|
1273
|
+
* * **`notify`.** The escalation channel is a repository prerequisite, refused by
|
|
1274
|
+
* name before creation; inventing one here would fake a verified channel.
|
|
1275
|
+
*/
|
|
1276
|
+
export function composeDefaultSetupEpicPolicy(args) {
|
|
1277
|
+
const policy = {};
|
|
1278
|
+
if (args.featureBranch !== undefined) {
|
|
1279
|
+
policy.base_branch = args.featureBranch;
|
|
1280
|
+
if (!args.attended) {
|
|
1281
|
+
policy.posture = SETUP_EPIC_UNATTENDED_POSTURE;
|
|
1282
|
+
policy[SETUP_EPIC_AUTO_MERGE_POLICY_KEY] = true;
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
if (args.reviewPolicy !== undefined) {
|
|
1286
|
+
policy.review_policy = { source: args.reviewPolicy };
|
|
1287
|
+
}
|
|
1288
|
+
return policy;
|
|
1289
|
+
}
|
|
1290
|
+
/**
|
|
1291
|
+
* Apply the unattended default to a policy a `--policy-file` supplied (BAPI-1102).
|
|
1292
|
+
*
|
|
1293
|
+
* `composeDefaultSetupEpicPolicy` covers the NO-FILE path. This covers the other
|
|
1294
|
+
* one, and without it the default was unreachable by any documented invocation:
|
|
1295
|
+
* a file bypasses that composer entirely, so a file declaring only
|
|
1296
|
+
* `notify.webhook_url` — the exact remedy the `webhook_unverified` refusal, the
|
|
1297
|
+
* readiness remediation, and the record all name — produced a policy with no
|
|
1298
|
+
* `posture`, was never gated, and created an ATTENDED run with no auto-merge
|
|
1299
|
+
* that then parked `v2_merge_approval_required` at every merge. Silently. The
|
|
1300
|
+
* only working unattended invocation was a hand-written file carrying `posture`,
|
|
1301
|
+
* `v2_auto_merge_enabled` and the webhook together, which nothing documents.
|
|
1302
|
+
*
|
|
1303
|
+
* `policyFileDeclaresUnattendedPosture` already states the intended contract in
|
|
1304
|
+
* its own docstring — "a file that stays silent about posture leaves that
|
|
1305
|
+
* default in play for `--attended` to decline" — and this is the code that makes
|
|
1306
|
+
* that true.
|
|
1307
|
+
*
|
|
1308
|
+
* FILL-IF-ABSENT, on the strictest reading of absent, and the boundaries are the
|
|
1309
|
+
* contract:
|
|
1310
|
+
*
|
|
1311
|
+
* * **The file declared a `posture`** → untouched, whatever it says. A file that
|
|
1312
|
+
* answered the question owns the answer; that is what `--policy-file` means,
|
|
1313
|
+
* and the one case where it contradicts `--attended` is already a named
|
|
1314
|
+
* refusal upstream (`attended_with_policy_file_posture`).
|
|
1315
|
+
* * **`--attended`** → untouched. It is the explicit opt-out from a default, and
|
|
1316
|
+
* this is the default it opts out of.
|
|
1317
|
+
* * **No feature branch** → untouched. Identical to the no-file composer's own
|
|
1318
|
+
* boundary: a run whose children merge straight into the repository base
|
|
1319
|
+
* branch must never be handed a posture and an auto-merge authorization.
|
|
1320
|
+
* * **`v2_auto_merge_enabled` already present** → left exactly as the file set
|
|
1321
|
+
* it, including `false`. Filling the posture does not overwrite an explicit
|
|
1322
|
+
* merge decision.
|
|
1323
|
+
*
|
|
1324
|
+
* Pure: it returns a new object and never mutates the input. It is applied on the
|
|
1325
|
+
* CREATE path only. A reused live run reconciles the file against its STORED
|
|
1326
|
+
* policy, and adding a posture there would let `--replace-policy` newly authorize
|
|
1327
|
+
* auto-merge on a run that never had it — the AC-3 guarantee this ticket's gate
|
|
1328
|
+
* placement already rests on.
|
|
1329
|
+
*/
|
|
1330
|
+
export function applySetupEpicUnattendedDefault(policy, args) {
|
|
1331
|
+
if (args.attended)
|
|
1332
|
+
return policy;
|
|
1333
|
+
if (args.featureBranch === undefined)
|
|
1334
|
+
return policy;
|
|
1335
|
+
if ("posture" in policy)
|
|
1336
|
+
return policy;
|
|
1337
|
+
const composed = { ...policy };
|
|
1338
|
+
composed.posture = SETUP_EPIC_UNATTENDED_POSTURE;
|
|
1339
|
+
if (!(SETUP_EPIC_AUTO_MERGE_POLICY_KEY in composed)) {
|
|
1340
|
+
composed[SETUP_EPIC_AUTO_MERGE_POLICY_KEY] = true;
|
|
1341
|
+
}
|
|
1342
|
+
return composed;
|
|
1343
|
+
}
|
|
1008
1344
|
/**
|
|
1009
1345
|
* One line naming the selected branch strategy AND why it was selected.
|
|
1010
1346
|
*
|
|
@@ -1013,14 +1349,30 @@ export function resolveFeatureBranchSelection(opts, planNodeCount, policyDeclare
|
|
|
1013
1349
|
* their policy file asked for it, or the multi-node default derived it — those
|
|
1014
1350
|
* three have different recovery steps when the name is not the one they wanted.
|
|
1015
1351
|
*/
|
|
1016
|
-
function describeBranchStrategy(opts, featureBranch, policyDeclaresBranch
|
|
1352
|
+
function describeBranchStrategy(opts, featureBranch, policyDeclaresBranch,
|
|
1353
|
+
/**
|
|
1354
|
+
* BAPI-1102 — `false` when a `--policy-file` owns the whole policy. The posture
|
|
1355
|
+
* suffix describes what THIS COMMAND composes, so claiming a posture over a
|
|
1356
|
+
* policy the file supplied would be a statement setup-epic cannot make: the
|
|
1357
|
+
* file may declare any posture, or none, and this function never reads it.
|
|
1358
|
+
*/
|
|
1359
|
+
composesDefaultPolicy = true) {
|
|
1017
1360
|
const cutNote = "cut on origin with local git at the canonical indexed commit; " +
|
|
1018
1361
|
"validated server-side at approval";
|
|
1362
|
+
// BAPI-1102: the posture is stated ALONGSIDE the branch, on the same line, and
|
|
1363
|
+
// only for the paths where this command actually chooses it. An operator
|
|
1364
|
+
// reading `epic/BAPI-1102` needs to know whether the run will drive itself.
|
|
1365
|
+
const posture = !composesDefaultPolicy
|
|
1366
|
+
? ""
|
|
1367
|
+
: opts.attended
|
|
1368
|
+
? "; ATTENDED posture (--attended): no auto-merge authorization"
|
|
1369
|
+
: "; UNATTENDED posture with auto-merge into the epic branch " +
|
|
1370
|
+
"(the integration PR stays human-gated) — pass --attended to opt out";
|
|
1019
1371
|
if (featureBranch !== undefined) {
|
|
1020
1372
|
if (opts.featureBranch !== undefined) {
|
|
1021
|
-
return `${featureBranch} (explicit --feature-branch override; ${cutNote})`;
|
|
1373
|
+
return `${featureBranch} (explicit --feature-branch override; ${cutNote}${posture})`;
|
|
1022
1374
|
}
|
|
1023
|
-
return `${featureBranch} (derived epic branch for this multi-ticket plan; ${cutNote})`;
|
|
1375
|
+
return `${featureBranch} (derived epic branch for this multi-ticket plan; ${cutNote}${posture})`;
|
|
1024
1376
|
}
|
|
1025
1377
|
if (opts.intoBase) {
|
|
1026
1378
|
return "repository base branch — epic branch disabled by --into-base; every child PR targets base";
|
|
@@ -1152,6 +1504,17 @@ export const SETUP_EPIC_PARSE_FIRST_MESSAGE = "parse the repository first";
|
|
|
1152
1504
|
* `baseBranch` slip) is deliberately NOT interpreted here: the server's strict
|
|
1153
1505
|
* `RunPolicy` validation owns rejecting it, so this returns `undefined` and the
|
|
1154
1506
|
* request proceeds to that 422 unchanged.
|
|
1507
|
+
*
|
|
1508
|
+
* BAPI-1127: SNAKE-CASE ONLY, on purpose, and a documented exception to the
|
|
1509
|
+
* "one persisted-run resolver" rule rather than a violation of it. The input
|
|
1510
|
+
* here is a REQUESTED policy document the operator is about to submit — not a
|
|
1511
|
+
* persisted run — so the right answer to the camelCase slip is to leave it
|
|
1512
|
+
* alone and let the server's strict boundary refuse it with a 422 the operator
|
|
1513
|
+
* can act on. Honoring the alias here would quietly repair a file the server is
|
|
1514
|
+
* about to reject, and the operator would never learn their file was wrong.
|
|
1515
|
+
* The canonical precedence for a PERSISTED run's declared branch lives in
|
|
1516
|
+
* `conductor/run-branch.ts#resolveDeclaredRunBaseBranch`; this function is not
|
|
1517
|
+
* it and must not grow into it.
|
|
1155
1518
|
*/
|
|
1156
1519
|
export function resolveEffectiveFeatureBranch(featureBranch, requestedPolicy) {
|
|
1157
1520
|
if (featureBranch !== undefined)
|
|
@@ -1292,7 +1655,29 @@ function describeScopeTransition(state, status) {
|
|
|
1292
1655
|
}
|
|
1293
1656
|
return `Scope: ${label}…`;
|
|
1294
1657
|
}
|
|
1658
|
+
/**
|
|
1659
|
+
* Run the whole `setup-epic` workflow and return its authoritative result.
|
|
1660
|
+
*
|
|
1661
|
+
* The internal entry point composed callers use. Behaviorally identical to
|
|
1662
|
+
* {@link runSetupEpicCli} in every observable way — same output, same refusals,
|
|
1663
|
+
* same exit codes — and differs only in what it hands back.
|
|
1664
|
+
*/
|
|
1665
|
+
export async function runSetupEpicWorkflow(argv, overrides = {}) {
|
|
1666
|
+
const sink = {};
|
|
1667
|
+
const exitCode = await executeSetupEpic(argv, overrides, sink);
|
|
1668
|
+
return {
|
|
1669
|
+
exitCode,
|
|
1670
|
+
...(sink.epicRunId !== undefined ? { epicRunId: sink.epicRunId } : {}),
|
|
1671
|
+
};
|
|
1672
|
+
}
|
|
1673
|
+
/**
|
|
1674
|
+
* The PUBLIC `setup-epic` entry point. Returns the process exit code and nothing
|
|
1675
|
+
* else, exactly as it always has.
|
|
1676
|
+
*/
|
|
1295
1677
|
export async function runSetupEpicCli(argv, overrides = {}) {
|
|
1678
|
+
return (await runSetupEpicWorkflow(argv, overrides)).exitCode;
|
|
1679
|
+
}
|
|
1680
|
+
async function executeSetupEpic(argv, overrides, runIdSink) {
|
|
1296
1681
|
const deps = { ...createDefaultSetupEpicDeps(), ...overrides };
|
|
1297
1682
|
const parsed = parseSetupEpicArgs(argv);
|
|
1298
1683
|
if (parsed.status === "help") {
|
|
@@ -1361,7 +1746,12 @@ export async function runSetupEpicCli(argv, overrides = {}) {
|
|
|
1361
1746
|
// network and asks no question, so running it here is what makes `--local-only`
|
|
1362
1747
|
// and an ordinary invocation report the same strategy for the same plan.
|
|
1363
1748
|
const featureBranch = resolveFeatureBranchSelection(opts, plan.nodes.length, policyDeclaresBranch);
|
|
1364
|
-
const branchStrategyLine = describeBranchStrategy(opts, featureBranch, policyDeclaresBranch
|
|
1749
|
+
const branchStrategyLine = describeBranchStrategy(opts, featureBranch, policyDeclaresBranch,
|
|
1750
|
+
// `filePolicy` is set exactly when `--policy-file` was supplied, and a
|
|
1751
|
+
// supplied file supersedes composition entirely — so this is the same
|
|
1752
|
+
// predicate as "this command composes the policy", available here, several
|
|
1753
|
+
// statements before the gate resolves `requestedPolicy`.
|
|
1754
|
+
filePolicy === undefined);
|
|
1365
1755
|
// --- Consolidated policy preflight (BAPI-1054) ---------------------------
|
|
1366
1756
|
// ONE gate, run BEFORE credential resolution and before any request, over the
|
|
1367
1757
|
// whole flag-backed choice-set. It sits above the `--local-only` return
|
|
@@ -1374,6 +1764,7 @@ export async function runSetupEpicCli(argv, overrides = {}) {
|
|
|
1374
1764
|
// still the better error for everything inside the policy file.
|
|
1375
1765
|
const policyGate = preflightSetupEpicPolicy({
|
|
1376
1766
|
intoBase: opts.intoBase,
|
|
1767
|
+
attended: opts.attended,
|
|
1377
1768
|
policyFile: opts.policyFile,
|
|
1378
1769
|
filePolicy,
|
|
1379
1770
|
policyDeclaresBranch,
|
|
@@ -1451,14 +1842,42 @@ export async function runSetupEpicCli(argv, overrides = {}) {
|
|
|
1451
1842
|
// that passed the gate — not a second composition — so the payload the create
|
|
1452
1843
|
// request carries is exactly what was validated.
|
|
1453
1844
|
const requestedPolicy = policyGate.policy;
|
|
1454
|
-
if (requestedPolicy !== undefined) {
|
|
1455
|
-
say(`Policy: ${opts.policyFile} (complete run policy, applied at creation)`);
|
|
1456
|
-
}
|
|
1457
1845
|
// --- Effective feature branch (BAPI-850) ----------------------------------
|
|
1458
1846
|
// Derived AFTER policy composition so a branch the policy file declares under
|
|
1459
1847
|
// `base_branch` is honored exactly as `--feature-branch` would be. A malformed
|
|
1460
1848
|
// or unknown policy field is left for the server's strict validation.
|
|
1461
1849
|
const effectiveFeatureBranch = resolveEffectiveFeatureBranch(featureBranch, requestedPolicy);
|
|
1850
|
+
// --- The policy the CREATE path will send (BAPI-1102) ---------------------
|
|
1851
|
+
// Composed HERE, as soon as both of its inputs exist, so the one object that
|
|
1852
|
+
// gets reported, gated, and sent is the same object throughout. It is consumed
|
|
1853
|
+
// only on the create branch below; the reuse branch reconciles the RAW file
|
|
1854
|
+
// against the run's stored policy, because adding a posture there would let
|
|
1855
|
+
// `--replace-policy` newly authorize auto-merge on a run that never had it.
|
|
1856
|
+
const composedForCreate = requestedPolicy !== undefined
|
|
1857
|
+
? applySetupEpicUnattendedDefault(requestedPolicy, {
|
|
1858
|
+
featureBranch: effectiveFeatureBranch,
|
|
1859
|
+
attended: opts.attended,
|
|
1860
|
+
})
|
|
1861
|
+
: composeDefaultSetupEpicPolicy({
|
|
1862
|
+
featureBranch,
|
|
1863
|
+
attended: opts.attended,
|
|
1864
|
+
reviewPolicy: opts.reviewPolicy,
|
|
1865
|
+
});
|
|
1866
|
+
if (requestedPolicy !== undefined) {
|
|
1867
|
+
// The posture is stated for the FILE path too, and read off the composed
|
|
1868
|
+
// object rather than re-derived. The `--policy-file` invocation is the one
|
|
1869
|
+
// the `webhook_unverified` remedy steers operators toward, so it was the one
|
|
1870
|
+
// path that could create an auto-merging run without ever printing that it
|
|
1871
|
+
// had — the exact thing this line exists to prevent. "Complete run policy"
|
|
1872
|
+
// is likewise no longer said when this command added the posture to it.
|
|
1873
|
+
const inherited = composedForCreate !== requestedPolicy
|
|
1874
|
+
? "; UNATTENDED posture with auto-merge into the epic branch composed by " +
|
|
1875
|
+
"default over a posture-silent file (the integration PR stays " +
|
|
1876
|
+
"human-gated) — pass --attended to opt out"
|
|
1877
|
+
: "";
|
|
1878
|
+
const shape = inherited === "" ? "complete run policy" : "run policy";
|
|
1879
|
+
say(`Policy: ${opts.policyFile} (${shape}, applied at creation${inherited})`);
|
|
1880
|
+
}
|
|
1462
1881
|
if (effectiveFeatureBranch !== undefined && featureBranch === undefined) {
|
|
1463
1882
|
say(`Feature: ${effectiveFeatureBranch} (from the policy file; cut on origin with local ` +
|
|
1464
1883
|
`git at the canonical indexed commit; validated server-side at approval)`);
|
|
@@ -1922,6 +2341,9 @@ export async function runSetupEpicCli(argv, overrides = {}) {
|
|
|
1922
2341
|
result.feature_branch = effectiveFeatureBranch;
|
|
1923
2342
|
// --- Step 1: create (only when there is no live run) ---------------------
|
|
1924
2343
|
if (existingRunId) {
|
|
2344
|
+
// BAPI-1102 — the reuse path's authoritative id, taken from the pre-check's
|
|
2345
|
+
// server response rather than from anything local.
|
|
2346
|
+
runIdSink.epicRunId = existingRunId;
|
|
1925
2347
|
say(`Run: reusing ${existingRunId} (status: ${existingStatus})`);
|
|
1926
2348
|
// A pre-check that already found a live run means create would answer 200
|
|
1927
2349
|
// anyway; reconcile the file against that run's stored policy directly.
|
|
@@ -1941,6 +2363,46 @@ export async function runSetupEpicCli(argv, overrides = {}) {
|
|
|
1941
2363
|
}
|
|
1942
2364
|
}
|
|
1943
2365
|
else {
|
|
2366
|
+
// --- BAPI-1102: the pre-create unattended prerequisite gate -------------
|
|
2367
|
+
//
|
|
2368
|
+
// Applied ONLY on the create path, and that placement is the AC-3 guarantee
|
|
2369
|
+
// in code: a reused live run keeps its stored policy and its dispatch
|
|
2370
|
+
// behavior untouched, so no deploy of this ticket can refuse or re-gate a
|
|
2371
|
+
// run that is already going.
|
|
2372
|
+
//
|
|
2373
|
+
// Evaluated against the COMPOSED policy, so it covers the default profile and
|
|
2374
|
+
// a `--policy-file` alike; skipped entirely for `--attended` and for any
|
|
2375
|
+
// policy that is not unattended, which is what keeps an attended run from
|
|
2376
|
+
// paying for a readiness read it does not need.
|
|
2377
|
+
//
|
|
2378
|
+
// The file branch runs through `applySetupEpicUnattendedDefault` rather than
|
|
2379
|
+
// taking the file verbatim: a file bypasses the no-file composer entirely, so
|
|
2380
|
+
// a posture-SILENT file used to skip this gate and create an attended run —
|
|
2381
|
+
// which made the default profile unreachable by any documented invocation.
|
|
2382
|
+
// The composed object is what gets gated AND what gets sent, so the create
|
|
2383
|
+
// request carries exactly what was validated.
|
|
2384
|
+
{
|
|
2385
|
+
if (requestedPolicyIsUnattended(composedForCreate, opts.attended)) {
|
|
2386
|
+
let readiness = null;
|
|
2387
|
+
try {
|
|
2388
|
+
readiness = await fetchConductorReadiness(access, deps.fetch);
|
|
2389
|
+
}
|
|
2390
|
+
catch {
|
|
2391
|
+
// Fail CLOSED, and silently as to WHY: the thrown value can carry a URL
|
|
2392
|
+
// or a response excerpt, and this refusal is rendered to a terminal. The
|
|
2393
|
+
// generic `readiness_unreadable` message says what to do about it.
|
|
2394
|
+
readiness = null;
|
|
2395
|
+
}
|
|
2396
|
+
// The COMPOSED policy, not the flags: a --policy-file declaring
|
|
2397
|
+
// notify.webhook_url satisfies the escalation-channel prerequisite, and
|
|
2398
|
+
// it is the same object the create request carries.
|
|
2399
|
+
const refusal = evaluateSetupEpicUnattendedPrerequisites(readiness, composedForCreate);
|
|
2400
|
+
if (refusal !== null) {
|
|
2401
|
+
deps.errorLog(`setup-epic REFUSED (${refusal.reason}): ${refusal.message}`);
|
|
2402
|
+
return 1;
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
1944
2406
|
try {
|
|
1945
2407
|
// Persist the confirmed feature branch as policy_json.base_branch ONLY when
|
|
1946
2408
|
// one was selected; otherwise keep the exact legacy create request shape.
|
|
@@ -1951,23 +2413,27 @@ export async function runSetupEpicCli(argv, overrides = {}) {
|
|
|
1951
2413
|
// BAPI-754: a --policy-file supersedes that ad-hoc composition entirely —
|
|
1952
2414
|
// `requestedPolicy` IS the composed result, flags included — so the whole
|
|
1953
2415
|
// policy reaches the server in the create that mints the run.
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
2416
|
+
//
|
|
2417
|
+
// BAPI-1102: a run that SELECTS A FEATURE BRANCH is composed unattended by
|
|
2418
|
+
// default, whether or not a policy file supplied the rest of the policy.
|
|
2419
|
+
// The gating on a feature branch being selected is the whole safety of it,
|
|
2420
|
+
// not an optimization — see `composeDefaultSetupEpicPolicy` and
|
|
2421
|
+
// `applySetupEpicUnattendedDefault`.
|
|
2422
|
+
//
|
|
2423
|
+
// This is the OBJECT THE GATE ABOVE JUST VALIDATED, not a second
|
|
2424
|
+
// composition of the same inputs. Recomposing here is how the two could
|
|
2425
|
+
// drift: the gate would pass on one policy and the server would receive
|
|
2426
|
+
// another.
|
|
2427
|
+
const policyJson = composedForCreate;
|
|
1966
2428
|
const createRequest = Object.keys(policyJson).length > 0 || requestedPolicy !== undefined
|
|
1967
2429
|
? { epicKey: opts.epicKey, policyJson }
|
|
1968
2430
|
: { epicKey: opts.epicKey };
|
|
1969
2431
|
const { run, created } = await createEpicRunWithDisposition(access, createRequest, deps.fetch);
|
|
1970
2432
|
result.epic_run_id = run.epic_run_id;
|
|
2433
|
+
// BAPI-1102 — the create path's authoritative id. `createEpicRunWithDisposition`
|
|
2434
|
+
// returns the server's run for a fresh creation AND for a create-race
|
|
2435
|
+
// adoption (`created: false`), so both are captured by this one assignment.
|
|
2436
|
+
runIdSink.epicRunId = run.epic_run_id;
|
|
1971
2437
|
result.status = run.status;
|
|
1972
2438
|
result.run_created = created;
|
|
1973
2439
|
if (created) {
|
|
@@ -2203,6 +2669,25 @@ export async function runSetupEpicCli(argv, overrides = {}) {
|
|
|
2203
2669
|
`dispatch has NOT started. Correct repository access or the branch ` +
|
|
2204
2670
|
`configuration, then re-run setup-epic.\nDetail: ${errorDetail(err)}`);
|
|
2205
2671
|
}
|
|
2672
|
+
else if (err instanceof ConductorBridgeApiError &&
|
|
2673
|
+
err.errorCode === SETUP_EPIC_UNATTENDED_POSTURE_REFUSED_CODE) {
|
|
2674
|
+
// BAPI-1102 — the one approval refusal with a canonical, non-obvious
|
|
2675
|
+
// remedy. Consent is read at BOTH boundaries, so a repository whose consent
|
|
2676
|
+
// was revoked between creation and approval refuses HERE, with the run left
|
|
2677
|
+
// exactly as it was: created, planning, unapproved, no activation, no
|
|
2678
|
+
// executor dispatch, and reusable once consent is restored.
|
|
2679
|
+
//
|
|
2680
|
+
// Named separately from the generic approval failure because the generic
|
|
2681
|
+
// message ("Failed to approve the plan: …") tells an operator to look at the
|
|
2682
|
+
// plan, which is the wrong place entirely.
|
|
2683
|
+
deps.errorLog(`The plan was NOT approved: this repository is no longer authorized for ` +
|
|
2684
|
+
`unattended runs. The run WAS created and is left in 'planning' with no ` +
|
|
2685
|
+
`activation and no executor dispatch — nothing was half-applied.\n` +
|
|
2686
|
+
`Restore \`unattended_conductor_allowed\` for this repository (see ` +
|
|
2687
|
+
`docs/claude/account-settings-operator-runbook.md) and re-run setup-epic, ` +
|
|
2688
|
+
`or deliberately convert this run to an attended posture through the ` +
|
|
2689
|
+
`supported operator flow.\nDetail: ${errorDetail(err)}`);
|
|
2690
|
+
}
|
|
2206
2691
|
else {
|
|
2207
2692
|
deps.errorLog(`Failed to approve the plan: ${errorDetail(err)}`);
|
|
2208
2693
|
}
|
|
@@ -2369,8 +2854,14 @@ export async function runSetupEpicCli(argv, overrides = {}) {
|
|
|
2369
2854
|
else {
|
|
2370
2855
|
say("The server-side reconciler will pick it up within ~30s.");
|
|
2371
2856
|
}
|
|
2372
|
-
|
|
2373
|
-
|
|
2857
|
+
// BAPI-1102 — the executor command is printed WITH a claim scope. Since
|
|
2858
|
+
// BAPI-1026 an executor started with no scope REFUSES TO START, so the
|
|
2859
|
+
// unscoped form printed here was guidance that could not work: an operator
|
|
2860
|
+
// who copied it got a startup error, and the natural reading of that error is
|
|
2861
|
+
// that the run is broken rather than that the instruction was.
|
|
2862
|
+
say("To execute this run's claimed jobs on this machine, run:");
|
|
2863
|
+
say(` npx -y ${MCP_PACKAGE_NAME} executor --repo ${access.repoName} ` +
|
|
2864
|
+
`--epic-run-id ${result.epic_run_id}`);
|
|
2374
2865
|
}
|
|
2375
2866
|
return exitCode;
|
|
2376
2867
|
}
|