@bridge_gpt/mcp-server 0.2.65 → 0.2.66
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 +25 -13
- package/build/agent-launchers/claude-executor-adapter.js +12 -18
- package/build/agent-launchers/executor-adapter.js +27 -0
- package/build/bridge-config.js +104 -0
- package/build/codex-bridge-profile.js +10 -1
- package/build/commands.generated.js +4 -4
- package/build/conductor/bridge-api-client.js +230 -10
- package/build/conductor/epic-child-continuation.js +61 -34
- package/build/conductor/epic-implementer-cli.js +12 -0
- package/build/conductor/guided-repair.js +323 -0
- package/build/conductor/readiness-cli.js +30 -9
- package/build/conductor/readiness.js +23 -1
- package/build/conductor/recovery-cli.js +507 -16
- package/build/conductor/recovery-operations.js +325 -2
- package/build/conductor/repair-contract.js +61 -3
- package/build/credential-store.js +5 -4
- package/build/diagnostic-text.js +36 -0
- package/build/docs.generated.js +1 -1
- package/build/doctor.js +70 -10
- package/build/drive-epic.js +395 -78
- package/build/epic-implementer-bin.js +24 -22
- package/build/epic-launch-diagnostic.js +31 -0
- package/build/epic-launch-profile.js +44 -14
- package/build/executor/completion-supervision.js +5 -0
- package/build/executor/correction-dispatch.js +60 -0
- package/build/executor/env.js +6 -0
- package/build/executor/http-client.js +15 -0
- package/build/executor/integration-job.js +6 -1
- package/build/executor/job-errors.js +13 -4
- package/build/executor/job-log-registry.js +340 -73
- package/build/executor/job-runner.js +260 -33
- package/build/executor/job-types.js +8 -13
- package/build/executor/preflight.js +91 -2
- package/build/executor/prompt-spec.js +113 -9
- package/build/executor/runner.js +23 -0
- package/build/executor/stale-artifacts.js +5 -4
- package/build/executor/types.js +1 -0
- package/build/executor/watch-cli.js +35 -8
- package/build/executor/worker-command.js +43 -43
- package/build/executor/worker-finalization.js +80 -15
- package/build/executor/worker-guard-hook.js +46 -10
- package/build/executor/worker-log.js +137 -70
- package/build/executor/worktree-gc.js +30 -6
- package/build/index.js +177 -329
- package/build/init.js +107 -4
- package/build/install-bridge.js +284 -20
- package/build/install-doctor.js +172 -30
- package/build/install-reexec.js +69 -7
- package/build/macos-terminal-permission-guidance.js +16 -0
- package/build/model-thresholds-tool.js +185 -0
- package/build/pipelines.generated.js +1 -1
- package/build/plane/status.js +4 -0
- package/build/plane/supervisor.js +4 -0
- package/build/plane/worker-exit-code.js +2 -0
- package/build/project-root.js +99 -0
- package/build/readme.generated.js +1 -1
- package/build/review-tickets.js +156 -13
- package/build/setup-epic.js +180 -108
- package/build/sfcc/setup-status.js +2 -0
- package/build/sfcc/tool-wrapper.js +2 -0
- package/build/sfcc/tools-configured.js +96 -0
- package/build/start-tickets.js +163 -58
- package/build/upgrade-cli.js +109 -24
- package/build/version.generated.js +3 -3
- package/build/windows-npm-entry.js +103 -0
- package/build/worker-guard-hook-bin.js +4 -3
- package/build/worktree-core.js +6 -5
- package/docs/install/mcp-tool-integrations.md +1 -0
- package/docs/install/sfcc-integration.md +12 -7
- package/package.json +1 -1
- package/pipelines/implement-ticket.json +1 -1
package/build/review-tickets.js
CHANGED
|
@@ -3,12 +3,22 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Reuses the `start-tickets` terminal spawn layer directly, running reviews in
|
|
5
5
|
* the current `deps.cwd`. Intentionally does NOT create Worktrunk worktrees,
|
|
6
|
-
* provision MCP registrations, run venv hooks
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
6
|
+
* provision MCP registrations, or run venv hooks. One terminal tab per ticket
|
|
7
|
+
* is spawned with `/review-ticket <KEY> [--auto] [--rounds=<1|2>]` as the agent
|
|
8
|
+
* prompt. `--rounds` is OMITTED by default so the backend's difficulty-adaptive
|
|
9
|
+
* review policy chooses the shape; it is only forwarded when the caller passes
|
|
10
|
+
* an explicit `--rounds`/`--review` round override.
|
|
11
|
+
*
|
|
12
|
+
* BAPI-1343 — review-threshold model routing for spawned tabs: an explicit
|
|
13
|
+
* `--model` always wins and no tier lookup is made. Otherwise each ticket's
|
|
14
|
+
* `/tickets/{n}/model-tier?consumer=review` is fetched (the shared bounded
|
|
15
|
+
* `start-tickets` fetcher, at most 3 at once), and a tab gets `--model` only
|
|
16
|
+
* when the server echoes `consumer: "review"` and reports a threshold-decided
|
|
17
|
+
* (`configured` or `anomaly`) tier, mapped to an alias exactly as
|
|
18
|
+
* `start-tickets` maps it. Legacy mode, a failed or timed-out lookup, or an
|
|
19
|
+
* older server that does not echo the consumer passes no `--model` (today's
|
|
20
|
+
* behavior); there is no premium fallback. `--dry-run` makes no lookup, and the
|
|
21
|
+
* epic-dispatch path below is not re-routed.
|
|
12
22
|
*
|
|
13
23
|
* Optionally supports a supervisor-driven epic-dispatch path: when an
|
|
14
24
|
* `EpicDispatchIdentity` is threaded through `ReviewTicketsOptions.epic`, each
|
|
@@ -18,7 +28,7 @@
|
|
|
18
28
|
* This path is worktree-free and base-branch-free; the human CLI path leaves
|
|
19
29
|
* `epic` unset and retains byte-for-byte identical behavior.
|
|
20
30
|
*/
|
|
21
|
-
import { createDefaultStartTicketsDeps, detectTerminal, runWithConcurrency, buildAgentInvocation, shSquoteInner, powershellSquote, isCommandOnPath, resolveFirstCommandOnPath, tmuxSessionNameForTicket, TICKET_KEY_PATTERN, DEFAULT_MAX_PARALLEL, validateBranchName, fetchAndResolveBaseSha, fetchStartTicketsConfigField, resolveStartTicketsBridgeApiAccess, buildCommandProvisioningDeps, buildMcpProvisioningDeps, } from "./start-tickets.js";
|
|
31
|
+
import { createDefaultStartTicketsDeps, detectTerminal, runWithConcurrency, buildAgentInvocation, shSquoteInner, powershellSquote, isCommandOnPath, resolveFirstCommandOnPath, tmuxSessionNameForTicket, TICKET_KEY_PATTERN, DEFAULT_MAX_PARALLEL, validateBranchName, fetchAndResolveBaseSha, fetchStartTicketsConfigField, resolveStartTicketsBridgeApiAccess, buildCommandProvisioningDeps, buildMcpProvisioningDeps, fetchTicketModelTiersForKeys, isThresholdDecidedTierResponse, normalizeDifficultyModelTierOverrides, fetchCodexModelCatalog, resolveRoutingAlias, validateResolvedModelAliasForAgent, } from "./start-tickets.js";
|
|
22
32
|
import { provisionCommandsForWorktree } from "./command-provisioning.js";
|
|
23
33
|
import { resolveCodexLaunchScope } from "./codex-launch-scope.js";
|
|
24
34
|
import { WINDOWS_TERMINAL_COMMAND, WINDOWS_POWERSHELL_CANDIDATES, TMUX_COMMAND, isSupportedStartTicketsPlatform, unsupportedPlatformMessage, } from "./start-tickets-prereqs.js";
|
|
@@ -48,7 +58,7 @@ export function getReviewTicketsUsage() {
|
|
|
48
58
|
" --rounds=1|2 Review depth: 1=single-pass, 2=full two-pass (default: adaptive — backend routes by difficulty)",
|
|
49
59
|
" --review KEY=auto,rounds=1 Per-ticket review mode override (repeatable)",
|
|
50
60
|
" --agent claude|cursor-agent|codex Agent command to launch in each tab (default: claude)",
|
|
51
|
-
" --model VALUE Model alias
|
|
61
|
+
" --model VALUE Model alias for every tab (always wins). Unset: each tab is routed by the review threshold; legacy mode or an older server passes no --model",
|
|
52
62
|
" --max-parallel N Max tabs to spawn concurrently (default: 3)",
|
|
53
63
|
" --dry-run Print intended commands; creates no tabs and opens no sessions",
|
|
54
64
|
" --base-branch BRANCH Base branch to fetch+pin for grounding (default: config base_branch, else \"main\")",
|
|
@@ -527,12 +537,125 @@ export function buildReviewPlanRows(deps, options, agent, status, overrides = {}
|
|
|
527
537
|
rounds: mode.rounds,
|
|
528
538
|
agentName: options.agentName,
|
|
529
539
|
modelAlias: options.modelAlias,
|
|
540
|
+
// BAPI-1343: plan rows (the dry-run preview) never perform a tier lookup;
|
|
541
|
+
// they show only an explicit --model, or the agent default.
|
|
542
|
+
...(!options.epic && options.modelAlias ? { modelRoutingReason: "explicit" } : {}),
|
|
530
543
|
status,
|
|
531
544
|
command,
|
|
532
545
|
runId,
|
|
533
546
|
};
|
|
534
547
|
});
|
|
535
548
|
}
|
|
549
|
+
/**
|
|
550
|
+
* BAPI-1343 — resolve each spawned tab's model from the review threshold.
|
|
551
|
+
*
|
|
552
|
+
* One `/model-tier?consumer=review` request per key through the shared
|
|
553
|
+
* `start-tickets` fetcher (bounded at 3, never throws). A tab is routed only
|
|
554
|
+
* when the server echoes `consumer: "review"` AND reports a threshold-decided
|
|
555
|
+
* tier; the tier maps to an alias exactly as `start-tickets` maps it (registry
|
|
556
|
+
* plus `difficulty_model_tier_overrides`, read once per batch; the Codex
|
|
557
|
+
* catalog, fetched once per batch; `validateResolvedModelAliasForAgent`, run
|
|
558
|
+
* at most once per distinct alias per batch). Every other outcome returns
|
|
559
|
+
* `modelAlias: null` (no `--model`, today's behavior) — there is no premium
|
|
560
|
+
* fallback. Callers handle explicit `--model`, `--dry-run`, and epic dispatch
|
|
561
|
+
* before calling this, so it always performs lookups.
|
|
562
|
+
*/
|
|
563
|
+
export async function resolveReviewModelRoutingForKeys(deps, keys, agent, maxParallel) {
|
|
564
|
+
const decisions = new Map();
|
|
565
|
+
const noModel = (reason) => ({ modelAlias: null, reason });
|
|
566
|
+
let accessResult;
|
|
567
|
+
try {
|
|
568
|
+
accessResult = await resolveStartTicketsBridgeApiAccess(deps);
|
|
569
|
+
}
|
|
570
|
+
catch {
|
|
571
|
+
for (const key of keys)
|
|
572
|
+
decisions.set(key, noModel("lookup-failed"));
|
|
573
|
+
return decisions;
|
|
574
|
+
}
|
|
575
|
+
if (!accessResult.ok) {
|
|
576
|
+
for (const key of keys)
|
|
577
|
+
decisions.set(key, noModel("lookup-failed"));
|
|
578
|
+
return decisions;
|
|
579
|
+
}
|
|
580
|
+
const access = accessResult.access;
|
|
581
|
+
const tierMap = await fetchTicketModelTiersForKeys(access, keys, maxParallel, { consumer: "review" });
|
|
582
|
+
// Batch-scoped alias inputs, loaded lazily so a batch with no
|
|
583
|
+
// threshold-decided ticket makes no config/catalog request.
|
|
584
|
+
let aliasInputs = null;
|
|
585
|
+
const loadAliasInputs = () => {
|
|
586
|
+
if (!aliasInputs) {
|
|
587
|
+
aliasInputs = (async () => {
|
|
588
|
+
try {
|
|
589
|
+
if (agent.name === "codex") {
|
|
590
|
+
const catalogResult = await fetchCodexModelCatalog(access);
|
|
591
|
+
return catalogResult.ok ? { overrides: null, catalog: catalogResult.catalog } : null;
|
|
592
|
+
}
|
|
593
|
+
const raw = await fetchStartTicketsConfigField(access, "difficulty_model_tier_overrides");
|
|
594
|
+
return { overrides: normalizeDifficultyModelTierOverrides(raw), catalog: null };
|
|
595
|
+
}
|
|
596
|
+
catch {
|
|
597
|
+
return null;
|
|
598
|
+
}
|
|
599
|
+
})();
|
|
600
|
+
}
|
|
601
|
+
return aliasInputs;
|
|
602
|
+
};
|
|
603
|
+
const validations = new Map();
|
|
604
|
+
const validateOnce = (alias) => {
|
|
605
|
+
let pending = validations.get(alias);
|
|
606
|
+
if (!pending) {
|
|
607
|
+
pending = validateResolvedModelAliasForAgent(deps, agent, alias).catch(() => ({ ok: false, warning: "model validation failed" }));
|
|
608
|
+
validations.set(alias, pending);
|
|
609
|
+
}
|
|
610
|
+
return pending;
|
|
611
|
+
};
|
|
612
|
+
for (const key of keys) {
|
|
613
|
+
const result = tierMap.get(key);
|
|
614
|
+
if (!result || !result.ok) {
|
|
615
|
+
decisions.set(key, noModel("lookup-failed"));
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
const value = result.value;
|
|
619
|
+
if (value.consumer !== "review") {
|
|
620
|
+
decisions.set(key, noModel("consumer-unrecognized"));
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
if (value.source === "fallback") {
|
|
624
|
+
// The server could not obtain a difficulty; its premium tier is a guess,
|
|
625
|
+
// and review routing has no premium fallback.
|
|
626
|
+
decisions.set(key, noModel("lookup-failed"));
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
if (!isThresholdDecidedTierResponse(value)) {
|
|
630
|
+
decisions.set(key, noModel("legacy"));
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
633
|
+
if (!value.tier) {
|
|
634
|
+
decisions.set(key, noModel("lookup-failed"));
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
if (!agent.supportsModelOverride) {
|
|
638
|
+
decisions.set(key, noModel("alias-unresolved"));
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
641
|
+
const inputs = await loadAliasInputs();
|
|
642
|
+
const candidate = inputs ? resolveRoutingAlias(agent, value.tier, inputs.overrides, inputs.catalog) : null;
|
|
643
|
+
if (!candidate) {
|
|
644
|
+
decisions.set(key, noModel("alias-unresolved"));
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
const validation = await validateOnce(candidate);
|
|
648
|
+
if (!validation.ok) {
|
|
649
|
+
decisions.set(key, noModel("alias-unresolved"));
|
|
650
|
+
continue;
|
|
651
|
+
}
|
|
652
|
+
decisions.set(key, {
|
|
653
|
+
modelAlias: validation.alias,
|
|
654
|
+
reason: value.thresholdMode === "anomaly" ? "review-threshold:anomaly" : "review-threshold:configured",
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
return decisions;
|
|
658
|
+
}
|
|
536
659
|
/** Build a dry-run preview base context from parsed CLI options only — never fetches. */
|
|
537
660
|
function previewBaseContextFromOptions(options) {
|
|
538
661
|
if (options.noRefreshBase)
|
|
@@ -590,10 +713,25 @@ export async function orchestrateReviewTickets(deps, options, overrides = {}) {
|
|
|
590
713
|
return { ok: false, error: baseContextResult.error };
|
|
591
714
|
}
|
|
592
715
|
const baseContext = baseContextResult.context;
|
|
716
|
+
// BAPI-1343: per-tab review-threshold routing for the human CLI path only.
|
|
717
|
+
// An explicit --model wins with no lookup; the epic-dispatch path keeps its
|
|
718
|
+
// pre-feature shape (Epic workers take their tier from the stamped payload).
|
|
719
|
+
let routing = null;
|
|
720
|
+
if (!options.epic) {
|
|
721
|
+
if (options.modelAlias) {
|
|
722
|
+
routing = new Map(options.keys.map((key) => [key, { modelAlias: options.modelAlias ?? null, reason: "explicit" }]));
|
|
723
|
+
}
|
|
724
|
+
else {
|
|
725
|
+
routing = await resolveReviewModelRoutingForKeys(deps, options.keys, agent, options.maxParallel);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
593
728
|
const terminal = detectTerminal(undefined, deps.env);
|
|
594
729
|
const rows = await runWithConcurrency(options.keys, options.maxParallel, async (key) => {
|
|
595
730
|
const mode = resolveEffectiveReviewMode(key, options);
|
|
596
|
-
const
|
|
731
|
+
const decision = routing?.get(key);
|
|
732
|
+
const modelAlias = decision ? decision.modelAlias : options.modelAlias;
|
|
733
|
+
const reasonField = decision ? { modelRoutingReason: decision.reason } : {};
|
|
734
|
+
const baseShellCommand = buildReviewAgentShellCommand(agent, key, mode, deps.platform, deps.cwd, modelAlias, baseContext, codexLaunchArgs);
|
|
597
735
|
let shellCommand = baseShellCommand;
|
|
598
736
|
let runId;
|
|
599
737
|
if (options.epic) {
|
|
@@ -608,7 +746,8 @@ export async function orchestrateReviewTickets(deps, options, overrides = {}) {
|
|
|
608
746
|
auto: mode.auto,
|
|
609
747
|
rounds: mode.rounds,
|
|
610
748
|
agentName: options.agentName,
|
|
611
|
-
modelAlias
|
|
749
|
+
modelAlias,
|
|
750
|
+
...reasonField,
|
|
612
751
|
status: "spawn-failed",
|
|
613
752
|
command: shellCommand,
|
|
614
753
|
runId,
|
|
@@ -625,7 +764,8 @@ export async function orchestrateReviewTickets(deps, options, overrides = {}) {
|
|
|
625
764
|
auto: mode.auto,
|
|
626
765
|
rounds: mode.rounds,
|
|
627
766
|
agentName: options.agentName,
|
|
628
|
-
modelAlias
|
|
767
|
+
modelAlias,
|
|
768
|
+
...reasonField,
|
|
629
769
|
status: "already-dispatched",
|
|
630
770
|
command: shellCommand,
|
|
631
771
|
runId,
|
|
@@ -645,7 +785,8 @@ export async function orchestrateReviewTickets(deps, options, overrides = {}) {
|
|
|
645
785
|
auto: mode.auto,
|
|
646
786
|
rounds: mode.rounds,
|
|
647
787
|
agentName: options.agentName,
|
|
648
|
-
modelAlias
|
|
788
|
+
modelAlias,
|
|
789
|
+
...reasonField,
|
|
649
790
|
status: "spawned",
|
|
650
791
|
command: shellCommand,
|
|
651
792
|
runId,
|
|
@@ -687,9 +828,11 @@ export function formatReviewTicketsSummaryReport(rows) {
|
|
|
687
828
|
const lines = ["Summary:"];
|
|
688
829
|
for (const row of rows) {
|
|
689
830
|
const modelPart = row.modelAlias ? row.modelAlias : "default";
|
|
831
|
+
// BAPI-1343: the closed, secret-free routing reason, when one was recorded.
|
|
832
|
+
const modelReasonPart = row.modelRoutingReason ? ` model_reason=${row.modelRoutingReason}` : "";
|
|
690
833
|
const roundsPart = row.rounds !== undefined ? String(row.rounds) : "adaptive";
|
|
691
834
|
const runIdPart = row.runId ? ` run_id=${row.runId}` : "";
|
|
692
|
-
lines.push(`${row.key} auto=${row.auto} rounds=${roundsPart} agent=${row.agentName} model=${modelPart} status=${row.status}${runIdPart}`);
|
|
835
|
+
lines.push(`${row.key} auto=${row.auto} rounds=${roundsPart} agent=${row.agentName} model=${modelPart}${modelReasonPart} status=${row.status}${runIdPart}`);
|
|
693
836
|
}
|
|
694
837
|
const failedRows = rows.filter((r) => r.status === "spawn-failed" || r.status === "already-dispatched");
|
|
695
838
|
if (failedRows.length > 0) {
|
package/build/setup-epic.js
CHANGED
|
@@ -78,7 +78,8 @@
|
|
|
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, buildConductorJiraUrl, conductorGetHeaders, createEpicRunWithDisposition, decodeCanonicalProvenance, fetchConductorReadiness, fetchEpicRunState, replaceEpicRunPolicy, resolveConductorBridgeApiAccess, resolveEpicLaunchBridgeApiAccess, storeEpicPlan, validateEpicPlan, ConductorBridgeApiError, } from "./conductor/bridge-api-client.js";
|
|
81
|
+
import { approveEpicPlan, buildConductorJiraUrl, conductorGetHeaders, createEpicRunWithDisposition, decodeCanonicalProvenance, fetchConductorReadiness, fetchEpicRunState, replaceEpicRunPolicy, resolveConductorBridgeApiAccess, resolveEpicLaunchBridgeApiAccess, safeDiagnosticMessage, storeEpicPlan, validateEpicPlan, ConductorBridgeApiError, } from "./conductor/bridge-api-client.js";
|
|
82
|
+
import { formatEpicLaunchChannelIdentity, getPinnedEpicLaunchPolicy, readEpicLaunchProfile, } from "./epic-launch-profile.js";
|
|
82
83
|
// BAPI-1133 (A2) — the single legacy-to-typed merge-execution-mode mapping and
|
|
83
84
|
// its total legacy resolver live in `merge-pull-request.ts` (no import cycle:
|
|
84
85
|
// that module never imports from `setup-epic.ts`). Reused here rather than
|
|
@@ -1117,6 +1118,24 @@ function findCycle(keys, adjacency) {
|
|
|
1117
1118
|
}
|
|
1118
1119
|
return null;
|
|
1119
1120
|
}
|
|
1121
|
+
export function effectivePolicyFacts(policy, source) {
|
|
1122
|
+
const object = policy && typeof policy === "object" && !Array.isArray(policy)
|
|
1123
|
+
? policy : {};
|
|
1124
|
+
const numbers = (value) => {
|
|
1125
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
1126
|
+
return null;
|
|
1127
|
+
const entries = Object.entries(value).filter(([key, item]) => /^[a-z][a-z0-9_]{0,63}$/.test(key) && typeof item === "number" && Number.isFinite(item));
|
|
1128
|
+
return entries.length ? Object.fromEntries(entries) : null;
|
|
1129
|
+
};
|
|
1130
|
+
return {
|
|
1131
|
+
source,
|
|
1132
|
+
serial_execution: "serial",
|
|
1133
|
+
posture: object.posture === "unattended" ? "unattended" : "attended",
|
|
1134
|
+
caps: numbers(object.caps),
|
|
1135
|
+
job_timeouts: numbers(object.job_timeouts),
|
|
1136
|
+
escalation_channel: formatEpicLaunchChannelIdentity(source === "stored" ? "unavailable" : policyDeclaresNotifyWebhook(object) ? "configured webhook" : "account email"),
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1120
1139
|
/** Propose a conductor epic feature branch name for an epic key (BAPI-655). */
|
|
1121
1140
|
export function proposeFeatureBranchName(epicKey) {
|
|
1122
1141
|
return `epic/${epicKey}`;
|
|
@@ -1561,12 +1580,7 @@ composesDefaultPolicy = true) {
|
|
|
1561
1580
|
`(pass --feature-branch to force ${proposeFeatureBranchName(opts.epicKey)} anyway)`);
|
|
1562
1581
|
}
|
|
1563
1582
|
function errorDetail(err) {
|
|
1564
|
-
|
|
1565
|
-
const status = err.status !== undefined ? ` (HTTP ${err.status})` : "";
|
|
1566
|
-
const preview = err.bodyPreview ? `: ${err.bodyPreview}` : "";
|
|
1567
|
-
return `${err.message}${status}${preview}`;
|
|
1568
|
-
}
|
|
1569
|
-
return err instanceof Error ? err.message : String(err);
|
|
1583
|
+
return safeDiagnosticMessage(err, "operation failed");
|
|
1570
1584
|
}
|
|
1571
1585
|
/**
|
|
1572
1586
|
* Reconcile a policy file against a REUSED run's stored policy, applying it only
|
|
@@ -1588,11 +1602,13 @@ async function applyReusedRunPolicy(args) {
|
|
|
1588
1602
|
});
|
|
1589
1603
|
if (outcome.kind === "noop") {
|
|
1590
1604
|
args.result.policy_applied = "noop";
|
|
1605
|
+
args.result.effective_policy = effectivePolicyFacts(args.storedPolicy, "stored");
|
|
1591
1606
|
args.say("Policy: policy unchanged");
|
|
1592
1607
|
return true;
|
|
1593
1608
|
}
|
|
1594
1609
|
if (outcome.kind === "refused_divergent") {
|
|
1595
1610
|
args.result.policy_applied = "refused_divergent";
|
|
1611
|
+
args.result.effective_policy = effectivePolicyFacts(args.storedPolicy, "stored");
|
|
1596
1612
|
args.deps.errorLog(`Epic run ${args.epicRunId} is live and its stored policy differs from the ` +
|
|
1597
1613
|
`policy file. setup-epic will not silently change a running epic's policy.\n` +
|
|
1598
1614
|
`${outcome.diffLines.join("\n")}\n` +
|
|
@@ -1605,10 +1621,12 @@ async function applyReusedRunPolicy(args) {
|
|
|
1605
1621
|
}
|
|
1606
1622
|
catch (err) {
|
|
1607
1623
|
args.result.policy_applied = "refused_divergent";
|
|
1624
|
+
args.result.effective_policy = effectivePolicyFacts(args.storedPolicy, "stored");
|
|
1608
1625
|
args.deps.errorLog(`Failed to replace the run policy: ${errorDetail(err)}`);
|
|
1609
1626
|
return false;
|
|
1610
1627
|
}
|
|
1611
1628
|
args.result.policy_applied = "replaced";
|
|
1629
|
+
args.result.effective_policy = effectivePolicyFacts(args.requestedPolicy, "explicit");
|
|
1612
1630
|
args.say("Policy: policy applied: replaced");
|
|
1613
1631
|
for (const line of outcome.diffLines)
|
|
1614
1632
|
args.say(line);
|
|
@@ -1860,12 +1878,13 @@ function describeScopeTransition(state, status) {
|
|
|
1860
1878
|
* {@link runSetupEpicCli} in every observable way — same output, same refusals,
|
|
1861
1879
|
* same exit codes — and differs only in what it hands back.
|
|
1862
1880
|
*/
|
|
1863
|
-
export async function runSetupEpicWorkflow(argv, overrides = {}) {
|
|
1881
|
+
export async function runSetupEpicWorkflow(argv, overrides = {}, handoff) {
|
|
1864
1882
|
const sink = {};
|
|
1865
|
-
const exitCode = await executeSetupEpic(argv, overrides, sink);
|
|
1883
|
+
const exitCode = await executeSetupEpic(argv, overrides, sink, handoff);
|
|
1866
1884
|
return {
|
|
1867
1885
|
exitCode,
|
|
1868
1886
|
...(sink.epicRunId !== undefined ? { epicRunId: sink.epicRunId } : {}),
|
|
1887
|
+
...(sink.epicRunId !== undefined ? { runExists: true } : {}),
|
|
1869
1888
|
};
|
|
1870
1889
|
}
|
|
1871
1890
|
/**
|
|
@@ -1875,7 +1894,7 @@ export async function runSetupEpicWorkflow(argv, overrides = {}) {
|
|
|
1875
1894
|
export async function runSetupEpicCli(argv, overrides = {}) {
|
|
1876
1895
|
return (await runSetupEpicWorkflow(argv, overrides)).exitCode;
|
|
1877
1896
|
}
|
|
1878
|
-
async function executeSetupEpic(argv, overrides, runIdSink) {
|
|
1897
|
+
async function executeSetupEpic(argv, overrides, runIdSink, handoff) {
|
|
1879
1898
|
const deps = { ...createDefaultSetupEpicDeps(), ...overrides };
|
|
1880
1899
|
const parsed = parseSetupEpicArgs(argv);
|
|
1881
1900
|
if (parsed.status === "help") {
|
|
@@ -1891,6 +1910,14 @@ async function executeSetupEpic(argv, overrides, runIdSink) {
|
|
|
1891
1910
|
const opts = parsed.options;
|
|
1892
1911
|
// With --json, stdout carries the single result object and nothing else.
|
|
1893
1912
|
const say = opts.json ? deps.errorLog : deps.log;
|
|
1913
|
+
if (handoff && (handoff.access.baseUrl !== handoff.selection.baseUrl ||
|
|
1914
|
+
handoff.access.repoName !== handoff.selection.repoName ||
|
|
1915
|
+
(opts.baseUrl !== undefined && opts.baseUrl !== handoff.selection.baseUrl) ||
|
|
1916
|
+
(opts.repo !== undefined && opts.repo !== handoff.selection.repoName) ||
|
|
1917
|
+
(opts.launchProfile !== undefined && opts.launchProfile !== handoff.selection.profileName))) {
|
|
1918
|
+
deps.errorLog("setup-epic REFUSED (selection-conflict): delegated target or repository disagrees with the pinned launch selection.");
|
|
1919
|
+
return 1;
|
|
1920
|
+
}
|
|
1894
1921
|
// --- Read + validate the plan sidecar (no network yet) -------------------
|
|
1895
1922
|
let raw;
|
|
1896
1923
|
try {
|
|
@@ -1922,18 +1949,137 @@ async function executeSetupEpic(argv, overrides, runIdSink) {
|
|
|
1922
1949
|
return 1;
|
|
1923
1950
|
}
|
|
1924
1951
|
const localHash = hashPlan(plan);
|
|
1952
|
+
// Resolve the selected target and live-run identity before choosing a policy.
|
|
1953
|
+
// The offline local-only path keeps its no-network contract.
|
|
1954
|
+
let access;
|
|
1955
|
+
let selectedProfilePolicyPath = handoff?.profile?.policyRef?.path;
|
|
1956
|
+
if (opts.localOnly && !handoff) {
|
|
1957
|
+
const profileName = opts.launchProfile ?? deps.env.BAPI_EPIC_LAUNCH_PROFILE?.trim();
|
|
1958
|
+
if (profileName) {
|
|
1959
|
+
const profileRead = await readEpicLaunchProfile(profileName, {
|
|
1960
|
+
env: deps.env,
|
|
1961
|
+
homedir: deps.homedir,
|
|
1962
|
+
readFile: deps.readFile,
|
|
1963
|
+
host: os.hostname(),
|
|
1964
|
+
});
|
|
1965
|
+
if (!profileRead.ok) {
|
|
1966
|
+
deps.errorLog(profileRead.message);
|
|
1967
|
+
return 1;
|
|
1968
|
+
}
|
|
1969
|
+
selectedProfilePolicyPath = profileRead.profile.policyRef?.path;
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
let existingRunId = null;
|
|
1973
|
+
let existingStatus = null;
|
|
1974
|
+
let existingBaseBranch = null;
|
|
1975
|
+
let existingReviewPolicy = null;
|
|
1976
|
+
// BAPI-754: the live run's SERVER-REDACTED stored policy, kept for the reuse
|
|
1977
|
+
// comparison. Redacted is the only form a client can ever see.
|
|
1978
|
+
let existingPolicyJson = null;
|
|
1979
|
+
if (!opts.localOnly) {
|
|
1980
|
+
// --- Resolve access ------------------------------------------------------
|
|
1981
|
+
// BAPI-1260: a caller that selected the epic-launch contract (drive-epic
|
|
1982
|
+
// forwards --launch-profile / --base-url) gets the target-first resolver and
|
|
1983
|
+
// the bound credential; the legacy resolver is kept byte-for-byte otherwise.
|
|
1984
|
+
const accessDeps = {
|
|
1985
|
+
env: deps.env,
|
|
1986
|
+
cwd: deps.cwd,
|
|
1987
|
+
homedir: deps.homedir,
|
|
1988
|
+
platform: deps.platform,
|
|
1989
|
+
readFile: deps.readFile,
|
|
1990
|
+
stat: deps.stat,
|
|
1991
|
+
repoName: opts.repo,
|
|
1992
|
+
};
|
|
1993
|
+
const accessResult = handoff
|
|
1994
|
+
? { ok: true, access: handoff.access, profile: handoff.profile }
|
|
1995
|
+
: opts.launchProfile !== undefined || opts.baseUrl !== undefined || Boolean(deps.env.BAPI_EPIC_LAUNCH_PROFILE?.trim())
|
|
1996
|
+
? await resolveEpicLaunchBridgeApiAccess({
|
|
1997
|
+
...(opts.launchProfile ? { profileName: opts.launchProfile } : {}),
|
|
1998
|
+
...(opts.baseUrl ? { baseUrl: opts.baseUrl } : {}),
|
|
1999
|
+
}, accessDeps)
|
|
2000
|
+
: await resolveConductorBridgeApiAccess(accessDeps);
|
|
2001
|
+
if (!accessResult.ok) {
|
|
2002
|
+
deps.errorLog(`Cannot reach the Bridge API: ${accessResult.error}`);
|
|
2003
|
+
return 1;
|
|
2004
|
+
}
|
|
2005
|
+
access = accessResult.access;
|
|
2006
|
+
say(`Epic: ${opts.epicKey}`);
|
|
2007
|
+
say(`Repo: ${access.repoName}`);
|
|
2008
|
+
say(`Plan: v${plan.plan_version}, ${plan.nodes.length} node(s), ${plan.edges.length} edge(s)`);
|
|
2009
|
+
say(`Local hash: ${localHash}`);
|
|
2010
|
+
for (const w of warnings)
|
|
2011
|
+
say(` [warn] ${w}`);
|
|
2012
|
+
selectedProfilePolicyPath = "profile" in accessResult
|
|
2013
|
+
? accessResult.profile?.policyRef?.path
|
|
2014
|
+
: undefined;
|
|
2015
|
+
// --- Step 0: pre-check ---------------------------------------------------
|
|
2016
|
+
// Never create on an ambiguous read. A wrong answer here mints a duplicate run.
|
|
2017
|
+
try {
|
|
2018
|
+
const state = await fetchEpicRunState(access, opts.epicKey, deps.fetch);
|
|
2019
|
+
existingRunId = state.epic_run?.epic_run_id ?? null;
|
|
2020
|
+
existingStatus = state.epic_run?.status ?? null;
|
|
2021
|
+
const existingPolicy = state.epic_run?.policy_json;
|
|
2022
|
+
existingPolicyJson = existingPolicy ?? null;
|
|
2023
|
+
const existingBase = existingPolicy && typeof existingPolicy === "object"
|
|
2024
|
+
? existingPolicy.base_branch
|
|
2025
|
+
: undefined;
|
|
2026
|
+
existingBaseBranch = typeof existingBase === "string" && existingBase.trim() !== ""
|
|
2027
|
+
? existingBase
|
|
2028
|
+
: null;
|
|
2029
|
+
// BAPI-679: read the existing run's review policy for the conflict guard
|
|
2030
|
+
// below. Absent/malformed reads as null — "the resolver's default applies" —
|
|
2031
|
+
// which is deliberately NOT the same as an explicitly selected source.
|
|
2032
|
+
const existingReview = existingPolicy && typeof existingPolicy === "object"
|
|
2033
|
+
? existingPolicy.review_policy
|
|
2034
|
+
: undefined;
|
|
2035
|
+
const existingReviewSource = existingReview && typeof existingReview === "object"
|
|
2036
|
+
? existingReview.source
|
|
2037
|
+
: undefined;
|
|
2038
|
+
existingReviewPolicy =
|
|
2039
|
+
typeof existingReviewSource === "string" && existingReviewSource.trim() !== ""
|
|
2040
|
+
? existingReviewSource
|
|
2041
|
+
: null;
|
|
2042
|
+
}
|
|
2043
|
+
catch (err) {
|
|
2044
|
+
if (err instanceof ConductorBridgeApiError && err.status === 404) {
|
|
2045
|
+
existingRunId = null; // No live run — the normal first-run path.
|
|
2046
|
+
}
|
|
2047
|
+
else if (err instanceof ConductorBridgeApiError && err.status === 409) {
|
|
2048
|
+
deps.errorLog(`Epic ${opts.epicKey} has MULTIPLE active runs — it is wedged, and every ` +
|
|
2049
|
+
`plan call will keep failing. Abandon the duplicate before retrying:\n` +
|
|
2050
|
+
` epic-implementer abandon-run --epic-run-id <duplicate_epic_run_id>\n` +
|
|
2051
|
+
`Detail: ${errorDetail(err)}`);
|
|
2052
|
+
return 1;
|
|
2053
|
+
}
|
|
2054
|
+
else {
|
|
2055
|
+
deps.errorLog(`Could not read the epic run state, so creating one would risk a duplicate ` +
|
|
2056
|
+
`(which wedges the epic permanently). Refusing to continue.\n` +
|
|
2057
|
+
`Detail: ${errorDetail(err)}`);
|
|
2058
|
+
return 1;
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
1925
2062
|
// --- Read the policy file (still no network) ------------------------------
|
|
1926
2063
|
// BAPI-754: parsed here so a malformed file, or a contradiction between the
|
|
1927
2064
|
// file and the CLI flags, fails before credentials are resolved or any request
|
|
1928
2065
|
// is dispatched.
|
|
1929
2066
|
let filePolicy;
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
2067
|
+
const selectedPolicyPath = opts.policyFile ?? (existingRunId === null ? selectedProfilePolicyPath : undefined);
|
|
2068
|
+
const selectedPolicySource = existingRunId !== null
|
|
2069
|
+
? "stored" : opts.policyFile !== undefined ? "explicit" : selectedProfilePolicyPath !== undefined ? "profile" : "default";
|
|
2070
|
+
if (selectedPolicyPath !== undefined) {
|
|
2071
|
+
const pinned = handoff ? getPinnedEpicLaunchPolicy(handoff.selection) : undefined;
|
|
2072
|
+
if (pinned !== undefined) {
|
|
2073
|
+
filePolicy = pinned;
|
|
2074
|
+
}
|
|
2075
|
+
else {
|
|
2076
|
+
const policyRead = await readSetupEpicPolicyFile(selectedPolicyPath, deps.readFile);
|
|
2077
|
+
if (!policyRead.ok) {
|
|
2078
|
+
deps.errorLog(policyRead.error);
|
|
2079
|
+
return 1;
|
|
2080
|
+
}
|
|
2081
|
+
filePolicy = policyRead.policy;
|
|
1935
2082
|
}
|
|
1936
|
-
filePolicy = policyRead.policy;
|
|
1937
2083
|
}
|
|
1938
2084
|
const policyDeclaresBranch = filePolicy !== undefined &&
|
|
1939
2085
|
["base_branch", "baseBranch"].some((key) => key in filePolicy && filePolicy[key] !== undefined && filePolicy[key] !== null);
|
|
@@ -1963,7 +2109,7 @@ async function executeSetupEpic(argv, overrides, runIdSink) {
|
|
|
1963
2109
|
const policyGate = preflightSetupEpicPolicy({
|
|
1964
2110
|
intoBase: opts.intoBase,
|
|
1965
2111
|
attended: opts.attended,
|
|
1966
|
-
policyFile:
|
|
2112
|
+
policyFile: selectedPolicyPath,
|
|
1967
2113
|
filePolicy,
|
|
1968
2114
|
policyDeclaresBranch,
|
|
1969
2115
|
featureBranch,
|
|
@@ -2001,42 +2147,14 @@ async function executeSetupEpic(argv, overrides, runIdSink) {
|
|
|
2001
2147
|
plan_version: plan.plan_version,
|
|
2002
2148
|
local_plan_hash: localHash,
|
|
2003
2149
|
server_validated: false,
|
|
2150
|
+
feature_branch: opts.intoBase ? null : resolveEffectiveFeatureBranch(featureBranch, policyGate.policy) ?? null,
|
|
2004
2151
|
policy_applied: "absent",
|
|
2152
|
+
effective_policy: effectivePolicyFacts(policyGate.policy, selectedPolicySource),
|
|
2005
2153
|
warnings,
|
|
2006
2154
|
}, null, 2));
|
|
2007
2155
|
}
|
|
2008
2156
|
return 0;
|
|
2009
2157
|
}
|
|
2010
|
-
// --- Resolve access ------------------------------------------------------
|
|
2011
|
-
// BAPI-1260: a caller that selected the epic-launch contract (drive-epic
|
|
2012
|
-
// forwards --launch-profile / --base-url) gets the target-first resolver and
|
|
2013
|
-
// the bound credential; the legacy resolver is kept byte-for-byte otherwise.
|
|
2014
|
-
const accessDeps = {
|
|
2015
|
-
env: deps.env,
|
|
2016
|
-
cwd: deps.cwd,
|
|
2017
|
-
homedir: deps.homedir,
|
|
2018
|
-
platform: deps.platform,
|
|
2019
|
-
readFile: deps.readFile,
|
|
2020
|
-
stat: deps.stat,
|
|
2021
|
-
repoName: opts.repo,
|
|
2022
|
-
};
|
|
2023
|
-
const accessResult = opts.launchProfile !== undefined || opts.baseUrl !== undefined
|
|
2024
|
-
? await resolveEpicLaunchBridgeApiAccess({
|
|
2025
|
-
...(opts.launchProfile ? { profileName: opts.launchProfile } : {}),
|
|
2026
|
-
...(opts.baseUrl ? { baseUrl: opts.baseUrl } : {}),
|
|
2027
|
-
}, accessDeps)
|
|
2028
|
-
: await resolveConductorBridgeApiAccess(accessDeps);
|
|
2029
|
-
if (!accessResult.ok) {
|
|
2030
|
-
deps.errorLog(`Cannot reach the Bridge API: ${accessResult.error}`);
|
|
2031
|
-
return 1;
|
|
2032
|
-
}
|
|
2033
|
-
const access = accessResult.access;
|
|
2034
|
-
say(`Epic: ${opts.epicKey}`);
|
|
2035
|
-
say(`Repo: ${access.repoName}`);
|
|
2036
|
-
say(`Plan: v${plan.plan_version}, ${plan.nodes.length} node(s), ${plan.edges.length} edge(s)`);
|
|
2037
|
-
say(`Local hash: ${localHash}`);
|
|
2038
|
-
for (const w of warnings)
|
|
2039
|
-
say(` [warn] ${w}`);
|
|
2040
2158
|
// --- Branch strategy (BAPI-1009) ----------------------------------------
|
|
2041
2159
|
// Already resolved above, before the network was touched. Reported HERE, ahead
|
|
2042
2160
|
// of any secondary policy detail, because it is the one decision that changes
|
|
@@ -2093,6 +2211,11 @@ async function executeSetupEpic(argv, overrides, runIdSink) {
|
|
|
2093
2211
|
reviewPolicy: opts.reviewPolicy,
|
|
2094
2212
|
mergeExecutionMode: declaredMergeExecutionMode,
|
|
2095
2213
|
});
|
|
2214
|
+
const selectedEffectivePolicyFacts = effectivePolicyFacts(existingRunId !== null ? existingPolicyJson : composedForCreate, selectedPolicySource);
|
|
2215
|
+
say(`Policy: effective ${selectedEffectivePolicyFacts.source}; serial execution; ` +
|
|
2216
|
+
`caps ${selectedEffectivePolicyFacts.caps ? JSON.stringify(selectedEffectivePolicyFacts.caps) : "unavailable"}; ` +
|
|
2217
|
+
`timeouts ${selectedEffectivePolicyFacts.job_timeouts ? JSON.stringify(selectedEffectivePolicyFacts.job_timeouts) : "unavailable"}; ` +
|
|
2218
|
+
`channel ${selectedEffectivePolicyFacts.escalation_channel}`);
|
|
2096
2219
|
// BAPI-1133 (A2). Reported only when THIS command stamped the key — i.e. it
|
|
2097
2220
|
// is present in the composed policy and was absent from whatever the
|
|
2098
2221
|
// operator supplied — as low-emphasis policy metadata, mirroring the
|
|
@@ -2128,59 +2251,6 @@ async function executeSetupEpic(argv, overrides, runIdSink) {
|
|
|
2128
2251
|
say(`Feature: ${effectiveFeatureBranch} (from the policy file; cut on origin with local ` +
|
|
2129
2252
|
`git at the canonical indexed commit; validated server-side at approval)`);
|
|
2130
2253
|
}
|
|
2131
|
-
// --- Step 0: pre-check ---------------------------------------------------
|
|
2132
|
-
// Never create on an ambiguous read. A wrong answer here mints a duplicate run.
|
|
2133
|
-
let existingRunId = null;
|
|
2134
|
-
let existingStatus = null;
|
|
2135
|
-
let existingBaseBranch = null;
|
|
2136
|
-
let existingReviewPolicy = null;
|
|
2137
|
-
// BAPI-754: the live run's SERVER-REDACTED stored policy, kept for the reuse
|
|
2138
|
-
// comparison. Redacted is the only form a client can ever see.
|
|
2139
|
-
let existingPolicyJson = null;
|
|
2140
|
-
try {
|
|
2141
|
-
const state = await fetchEpicRunState(access, opts.epicKey, deps.fetch);
|
|
2142
|
-
existingRunId = state.epic_run?.epic_run_id ?? null;
|
|
2143
|
-
existingStatus = state.epic_run?.status ?? null;
|
|
2144
|
-
const existingPolicy = state.epic_run?.policy_json;
|
|
2145
|
-
existingPolicyJson = existingPolicy ?? null;
|
|
2146
|
-
const existingBase = existingPolicy && typeof existingPolicy === "object"
|
|
2147
|
-
? existingPolicy.base_branch
|
|
2148
|
-
: undefined;
|
|
2149
|
-
existingBaseBranch = typeof existingBase === "string" && existingBase.trim() !== ""
|
|
2150
|
-
? existingBase
|
|
2151
|
-
: null;
|
|
2152
|
-
// BAPI-679: read the existing run's review policy for the conflict guard
|
|
2153
|
-
// below. Absent/malformed reads as null — "the resolver's default applies" —
|
|
2154
|
-
// which is deliberately NOT the same as an explicitly selected source.
|
|
2155
|
-
const existingReview = existingPolicy && typeof existingPolicy === "object"
|
|
2156
|
-
? existingPolicy.review_policy
|
|
2157
|
-
: undefined;
|
|
2158
|
-
const existingReviewSource = existingReview && typeof existingReview === "object"
|
|
2159
|
-
? existingReview.source
|
|
2160
|
-
: undefined;
|
|
2161
|
-
existingReviewPolicy =
|
|
2162
|
-
typeof existingReviewSource === "string" && existingReviewSource.trim() !== ""
|
|
2163
|
-
? existingReviewSource
|
|
2164
|
-
: null;
|
|
2165
|
-
}
|
|
2166
|
-
catch (err) {
|
|
2167
|
-
if (err instanceof ConductorBridgeApiError && err.status === 404) {
|
|
2168
|
-
existingRunId = null; // No live run — the normal first-run path.
|
|
2169
|
-
}
|
|
2170
|
-
else if (err instanceof ConductorBridgeApiError && err.status === 409) {
|
|
2171
|
-
deps.errorLog(`Epic ${opts.epicKey} has MULTIPLE active runs — it is wedged, and every ` +
|
|
2172
|
-
`plan call will keep failing. Abandon the duplicate before retrying:\n` +
|
|
2173
|
-
` epic-implementer abandon-run --epic-run-id <duplicate_epic_run_id>\n` +
|
|
2174
|
-
`Detail: ${errorDetail(err)}`);
|
|
2175
|
-
return 1;
|
|
2176
|
-
}
|
|
2177
|
-
else {
|
|
2178
|
-
deps.errorLog(`Could not read the epic run state, so creating one would risk a duplicate ` +
|
|
2179
|
-
`(which wedges the epic permanently). Refusing to continue.\n` +
|
|
2180
|
-
`Detail: ${errorDetail(err)}`);
|
|
2181
|
-
return 1;
|
|
2182
|
-
}
|
|
2183
|
-
}
|
|
2184
2254
|
// --- Feature-branch conflict guard against an existing live run ----------
|
|
2185
2255
|
// A selected branch that disagrees with the existing run's stored base branch
|
|
2186
2256
|
// must fail closed rather than patch or silently change that run. A matching
|
|
@@ -2508,6 +2578,7 @@ async function executeSetupEpic(argv, overrides, runIdSink) {
|
|
|
2508
2578
|
// "absent"; server_validated says whether the check was authoritative.
|
|
2509
2579
|
server_validated: validated !== null,
|
|
2510
2580
|
policy_applied: "absent",
|
|
2581
|
+
effective_policy: selectedEffectivePolicyFacts,
|
|
2511
2582
|
...(validated
|
|
2512
2583
|
? {
|
|
2513
2584
|
server_plan_hash: validated.planHash,
|
|
@@ -2579,6 +2650,7 @@ async function executeSetupEpic(argv, overrides, runIdSink) {
|
|
|
2579
2650
|
plan_approved: false,
|
|
2580
2651
|
server_validated: validated !== null,
|
|
2581
2652
|
policy_applied: "absent",
|
|
2653
|
+
effective_policy: selectedEffectivePolicyFacts,
|
|
2582
2654
|
warnings,
|
|
2583
2655
|
};
|
|
2584
2656
|
// BAPI-850: the EFFECTIVE feature branch (flag or policy file), so a
|
|
@@ -3128,14 +3200,14 @@ async function executeSetupEpic(argv, overrides, runIdSink) {
|
|
|
3128
3200
|
else {
|
|
3129
3201
|
say("The server-side reconciler will pick it up within ~30s.");
|
|
3130
3202
|
}
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3203
|
+
if (handoff) {
|
|
3204
|
+
say("The launcher starts or reuses the executor appropriate for this run.");
|
|
3205
|
+
}
|
|
3206
|
+
else {
|
|
3207
|
+
say("To execute this run's claimed jobs on this machine, run:");
|
|
3208
|
+
say(` npx -y ${MCP_PACKAGE_NAME} executor --repo ${access.repoName} ` +
|
|
3209
|
+
`--epic-run-id ${result.epic_run_id}`);
|
|
3210
|
+
}
|
|
3139
3211
|
}
|
|
3140
3212
|
return exitCode;
|
|
3141
3213
|
}
|