@bridge_gpt/mcp-server 0.2.60 → 0.2.65
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 +58 -11
- package/build/agent-launchers/claude-executor-adapter.js +134 -3
- package/build/agent-launchers/executor-adapter.js +54 -2
- package/build/agents.generated.js +10 -1
- package/build/bounded-wait.js +32 -1
- package/build/commands.generated.js +8 -7
- package/build/conductor/bridge-api-client.js +678 -14
- package/build/conductor/bring-up-facts.js +3 -1
- package/build/conductor/doctor.js +23 -14
- package/build/conductor/epic-child-continuation.js +324 -0
- package/build/conductor/epic-implementer-cli.js +142 -17
- package/build/conductor/git-hooks.js +529 -118
- package/build/conductor/hook-runtime-qualification.js +340 -0
- package/build/conductor/merge-execution-contract.js +30 -5
- package/build/conductor/merge-execution-eligibility.js +10 -9
- package/build/conductor/readiness-sections.js +104 -20
- package/build/conductor/readiness.js +129 -25
- package/build/conductor/recovery-cli.js +354 -13
- package/build/conductor/recovery-operations.js +251 -5
- package/build/conductor/repair-contract.js +8 -0
- package/build/conductor-bundle-artifacts.js +1 -0
- package/build/credential-store.js +194 -0
- package/build/docs.generated.js +2 -1
- package/build/doctor.js +161 -7
- package/build/drive-epic-key.js +12 -0
- package/build/drive-epic.js +1318 -111
- package/build/epic-coherence-input-cli.js +137 -0
- package/build/epic-coherence-input.js +920 -0
- package/build/epic-coherence-input.test-fixtures.js +175 -0
- package/build/epic-implementer-bin.js +35 -29
- package/build/epic-implementer-bundle-cli.js +14 -1
- package/build/epic-integration-pr.js +61 -15
- package/build/epic-launch-profile.js +464 -0
- package/build/epic-run-status.js +5 -0
- package/build/epic-shared/bridge-client.js +46 -4
- package/build/executor/cli.js +48 -0
- package/build/executor/completion-contract.js +423 -0
- package/build/executor/completion-supervision.js +286 -0
- package/build/executor/credentials.js +19 -1
- package/build/executor/deps.js +65 -1
- package/build/executor/env.js +6 -0
- package/build/executor/heartbeat.js +13 -3
- package/build/executor/http-client.js +117 -1
- package/build/executor/integration-job.js +759 -0
- package/build/executor/job-errors.js +48 -0
- package/build/executor/job-runner.js +1386 -35
- package/build/executor/job-types.js +10 -2
- package/build/executor/observation.js +114 -7
- package/build/executor/preflight.js +99 -0
- package/build/executor/process-containment.js +756 -0
- package/build/executor/process.js +235 -5
- package/build/executor/prompt-spec.js +262 -1
- package/build/executor/provider-checks.js +69 -0
- package/build/executor/rebase-finalization.js +351 -0
- package/build/executor/reconcile-overlap-job.js +77 -84
- package/build/executor/required-verification.js +313 -0
- package/build/executor/required-work-ledger.js +1121 -0
- package/build/executor/results.js +142 -0
- package/build/executor/resume-pre-spawn.js +87 -4
- package/build/executor/runner.js +246 -1
- package/build/executor/test-declaration.js +20 -0
- package/build/executor/types.js +55 -0
- package/build/executor/worker-command.js +49 -13
- package/build/executor/worker-finalization.js +88 -11
- package/build/executor/worker-guard-hook.js +29 -4
- package/build/executor/worktree.js +13 -0
- package/build/executor-launch.js +658 -0
- package/build/index.js +772 -954
- package/build/install-bridge-conductor.js +622 -40
- package/build/install-bridge.js +92 -16
- package/build/merge-pull-request.js +1 -1
- package/build/merge-workflow-config.js +54 -0
- package/build/merge-workflow-drift.js +35 -4
- package/build/merge-workflow.js +386 -2
- package/build/pipelines.generated.js +9 -8
- package/build/plan-discovery.js +137 -0
- package/build/plan-epic-implementer-eligibility.js +2 -2
- package/build/plane/cli.js +37 -3
- package/build/plane/member-logs.js +3 -0
- package/build/plane/member-roster.js +7 -1
- package/build/plane/shutdown.js +59 -1
- package/build/plane/supervisor.js +148 -0
- package/build/readme.generated.js +1 -1
- package/build/scripts/executor-protocol-contract-driver.js +13 -3
- package/build/setup-epic.js +130 -48
- package/build/version.generated.js +3 -3
- package/build/worker-guard-hook-bin.js +2 -2
- package/docs/CONDUCTOR.md +116 -2
- package/package.json +2 -1
|
@@ -106,10 +106,16 @@
|
|
|
106
106
|
* The command does NOT persist a repository-level review policy: that is
|
|
107
107
|
* BAPI-694's. The policy selected here is per-run and is reported as such.
|
|
108
108
|
*/
|
|
109
|
-
import { bootstrapConductorSupervisorDefaults,
|
|
109
|
+
import { bootstrapConductorSupervisorDefaults, classifySupervisorBootstrapFailure, fetchConductorReadiness, previewConductorSupervisorBootstrap, } from "./conductor/bridge-api-client.js";
|
|
110
110
|
import { CONDUCTOR_OPERATOR_RUNBOOK_POINTER, EXECUTOR_PROVISIONING_GUIDANCE, conductorInstallDoctorExitCode, conductorInstallDoctorHasFatal, formatConductorInstallDoctorReport, } from "./conductor/install-doctor.js";
|
|
111
111
|
import { describeNativeLedgerAvailability, } from "./conductor/doctor.js";
|
|
112
112
|
import { CONDUCTOR_BIN_UNRESOLVED_ERROR, MANAGED_HOOK_NAMES, } from "./conductor/git-hooks.js";
|
|
113
|
+
import os from "node:os";
|
|
114
|
+
import path from "node:path";
|
|
115
|
+
import { readFile as readFileAsync, stat as statAsync } from "node:fs/promises";
|
|
116
|
+
import { bindEpicLaunchCredential, readStoredBinding, } from "./credential-store.js";
|
|
117
|
+
import { EPIC_LAUNCH_EXECUTION_MODES, EPIC_LAUNCH_PROFILE_NAME_PATTERN, EPIC_LAUNCH_PROFILE_VERSION, createNodeEpicLaunchStoreDeps, readEpicLaunchProfile, renderEpicLaunchCommand, resolveEpicLaunchTarget, writeEpicLaunchProfile, } from "./epic-launch-profile.js";
|
|
118
|
+
import { DEFAULT_BAPI_BASE_URL } from "./executor/credentials.js";
|
|
113
119
|
import { MCP_PACKAGE_NAME } from "./mcp-identity.js";
|
|
114
120
|
import { CLAUDE_REVIEW_WORKFLOW_RELPATH, renderClaudeReviewWorkflow, } from "./claude-review-workflow.js";
|
|
115
121
|
import { formatExecutorInstallPreflight, } from "./executor/install-preflight.js";
|
|
@@ -161,6 +167,25 @@ export function getInstallBridgeConductorUsage() {
|
|
|
161
167
|
" --secret-name <NAME> claude-review workflow: OAuth token secret NAME",
|
|
162
168
|
" --branches a,b claude-review workflow: PR branch triggers",
|
|
163
169
|
" --skip-workflow Never offer the claude-review workflow",
|
|
170
|
+
" --replace-done-gate Replace an EXISTING done gate with the CI-only gate",
|
|
171
|
+
" built from --required-check. Without it, a repeat",
|
|
172
|
+
" install keeps the stored gate and only adds missing",
|
|
173
|
+
" checks. Any requirement a replacement would remove",
|
|
174
|
+
" is previewed and needs its own exact approval.",
|
|
175
|
+
" --launch-profile <name> (BAPI-1260) Opt in to the reusable epic launch: after",
|
|
176
|
+
" every mandatory prerequisite is verified, persist a",
|
|
177
|
+
" host-local profile and print the ONE drive-epic",
|
|
178
|
+
" command that selects it (runs in a fresh shell).",
|
|
179
|
+
" Optional for production — drive-epic targets it with",
|
|
180
|
+
" no profile. Needed for local or durable-host targets.",
|
|
181
|
+
" --base-url <url> Explicit Bridge API target for --launch-profile.",
|
|
182
|
+
" Required there (or BAPI_BASE_URL): guided setup",
|
|
183
|
+
" approves one named server and has no default.",
|
|
184
|
+
" --execution-mode <mode> local | durable (default: durable with",
|
|
185
|
+
" --executor-id, otherwise local).",
|
|
186
|
+
" --policy-file <path> Run policy the profile references; an unattended",
|
|
187
|
+
" run needs notify.webhook_url reachable from the",
|
|
188
|
+
" API server (notify.local_sink is not enough).",
|
|
164
189
|
" --dry-run Diagnose and preview only. Performs NO writes:",
|
|
165
190
|
" no POST, no file write, no host MCP config write,",
|
|
166
191
|
" no Git hook write, no install-state or capability",
|
|
@@ -244,6 +269,7 @@ export function parseInstallBridgeConductorArgs(argv) {
|
|
|
244
269
|
let repo;
|
|
245
270
|
let dryRun = false;
|
|
246
271
|
let skipWorkflow = false;
|
|
272
|
+
let replaceDoneGate = false;
|
|
247
273
|
let reviewPolicy = DEFAULT_CONDUCTOR_REVIEW_POLICY;
|
|
248
274
|
let reviewPolicyExplicit = false;
|
|
249
275
|
let epicKey;
|
|
@@ -255,6 +281,10 @@ export function parseInstallBridgeConductorArgs(argv) {
|
|
|
255
281
|
let secretName;
|
|
256
282
|
let branches;
|
|
257
283
|
const requiredChecks = [];
|
|
284
|
+
let launchProfile;
|
|
285
|
+
let baseUrl;
|
|
286
|
+
let executionMode;
|
|
287
|
+
let policyFile;
|
|
258
288
|
const takeValue = (arg, inline, next) => {
|
|
259
289
|
if (inline !== undefined) {
|
|
260
290
|
if (inline.trim().length === 0)
|
|
@@ -282,6 +312,11 @@ export function parseInstallBridgeConductorArgs(argv) {
|
|
|
282
312
|
return { status: "error", message: "--skip-workflow takes no value" };
|
|
283
313
|
skipWorkflow = true;
|
|
284
314
|
break;
|
|
315
|
+
case "--replace-done-gate":
|
|
316
|
+
if (inline !== undefined)
|
|
317
|
+
return { status: "error", message: "--replace-done-gate takes no value" };
|
|
318
|
+
replaceDoneGate = true;
|
|
319
|
+
break;
|
|
285
320
|
case "--repo-wide":
|
|
286
321
|
if (inline !== undefined)
|
|
287
322
|
return { status: "error", message: "--repo-wide takes no value" };
|
|
@@ -297,7 +332,11 @@ export function parseInstallBridgeConductorArgs(argv) {
|
|
|
297
332
|
case "--epic-run-id":
|
|
298
333
|
case "--allowed-bot":
|
|
299
334
|
case "--secret-name":
|
|
300
|
-
case "--branches":
|
|
335
|
+
case "--branches":
|
|
336
|
+
case "--launch-profile":
|
|
337
|
+
case "--base-url":
|
|
338
|
+
case "--execution-mode":
|
|
339
|
+
case "--policy-file": {
|
|
301
340
|
const taken = takeValue(flag, inline, argv[i + 1]);
|
|
302
341
|
if (!taken.ok)
|
|
303
342
|
return { status: "error", message: taken.error };
|
|
@@ -322,6 +361,28 @@ export function parseInstallBridgeConductorArgs(argv) {
|
|
|
322
361
|
allowedBot = value;
|
|
323
362
|
else if (flag === "--secret-name")
|
|
324
363
|
secretName = value;
|
|
364
|
+
else if (flag === "--launch-profile") {
|
|
365
|
+
if (!EPIC_LAUNCH_PROFILE_NAME_PATTERN.test(value)) {
|
|
366
|
+
return {
|
|
367
|
+
status: "error",
|
|
368
|
+
message: "--launch-profile must match [a-z0-9][a-z0-9._-]{0,62}.",
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
launchProfile = value;
|
|
372
|
+
}
|
|
373
|
+
else if (flag === "--base-url")
|
|
374
|
+
baseUrl = value;
|
|
375
|
+
else if (flag === "--policy-file")
|
|
376
|
+
policyFile = value;
|
|
377
|
+
else if (flag === "--execution-mode") {
|
|
378
|
+
if (!EPIC_LAUNCH_EXECUTION_MODES.includes(value)) {
|
|
379
|
+
return {
|
|
380
|
+
status: "error",
|
|
381
|
+
message: `Invalid --execution-mode '${value}'. Expected one of: ${EPIC_LAUNCH_EXECUTION_MODES.join(", ")}`,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
executionMode = value;
|
|
385
|
+
}
|
|
325
386
|
else if (flag === "--branches") {
|
|
326
387
|
const list = splitList(value);
|
|
327
388
|
if (list.length === 0)
|
|
@@ -404,6 +465,20 @@ export function parseInstallBridgeConductorArgs(argv) {
|
|
|
404
465
|
"--executor-id.",
|
|
405
466
|
};
|
|
406
467
|
}
|
|
468
|
+
// BAPI-1260 — the launch-profile inputs only mean something together.
|
|
469
|
+
if (launchProfile === undefined && (baseUrl !== undefined || executionMode !== undefined)) {
|
|
470
|
+
return {
|
|
471
|
+
status: "error",
|
|
472
|
+
message: "--base-url and --execution-mode configure an epic launch profile, so they require --launch-profile.",
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
if (executionMode === "durable" && executorId === undefined) {
|
|
476
|
+
return {
|
|
477
|
+
status: "error",
|
|
478
|
+
message: "--execution-mode durable requires --executor-id: a durable launch is served by a provisioned " +
|
|
479
|
+
"executor service, and this command proves its server-observed handshake before approving it.",
|
|
480
|
+
};
|
|
481
|
+
}
|
|
407
482
|
return {
|
|
408
483
|
status: "ok",
|
|
409
484
|
options: {
|
|
@@ -418,12 +493,38 @@ export function parseInstallBridgeConductorArgs(argv) {
|
|
|
418
493
|
secretName,
|
|
419
494
|
branches,
|
|
420
495
|
skipWorkflow,
|
|
496
|
+
...(replaceDoneGate ? { replaceDoneGate: true } : {}),
|
|
421
497
|
executorId,
|
|
422
498
|
...(epicRunIds.length > 0 ? { epicRunIds } : {}),
|
|
423
499
|
...(repoWide ? { repoWide: true } : {}),
|
|
500
|
+
...(launchProfile ? { launchProfile } : {}),
|
|
501
|
+
...(baseUrl ? { baseUrl } : {}),
|
|
502
|
+
...(launchProfile
|
|
503
|
+
? { executionMode: executionMode ?? (executorId !== undefined ? "durable" : "local") }
|
|
504
|
+
: {}),
|
|
505
|
+
...(policyFile ? { policyFile } : {}),
|
|
424
506
|
},
|
|
425
507
|
};
|
|
426
508
|
}
|
|
509
|
+
/** Real host-local implementations of the launch-profile seams. */
|
|
510
|
+
export function createDefaultLaunchProfileDeps(env) {
|
|
511
|
+
const store = createNodeEpicLaunchStoreDeps(env);
|
|
512
|
+
return {
|
|
513
|
+
hostname: () => os.hostname(),
|
|
514
|
+
nowIso: () => new Date().toISOString(),
|
|
515
|
+
readProfile: (name) => readEpicLaunchProfile(name, store),
|
|
516
|
+
writeProfile: (profile) => writeEpicLaunchProfile(profile, store),
|
|
517
|
+
readCredentialBinding: (repoName) => readStoredBinding(`bapi:${repoName}`, {
|
|
518
|
+
env,
|
|
519
|
+
homedir: store.homedir,
|
|
520
|
+
platform: store.platform,
|
|
521
|
+
readFile: store.readFile,
|
|
522
|
+
stat: (p) => statAsync(p),
|
|
523
|
+
}),
|
|
524
|
+
bindCredential: (repoName, baseUrl) => bindEpicLaunchCredential(repoName, baseUrl, store),
|
|
525
|
+
readPolicyFile: (absolutePath) => readFileAsync(absolutePath, "utf-8"),
|
|
526
|
+
};
|
|
527
|
+
}
|
|
427
528
|
// ---------------------------------------------------------------------------
|
|
428
529
|
// Bounded readiness observation (BAPI-779)
|
|
429
530
|
// ---------------------------------------------------------------------------
|
|
@@ -495,6 +596,89 @@ export async function observeExecutorReadiness(access, fetchJson, deps, options
|
|
|
495
596
|
// Runner
|
|
496
597
|
// ---------------------------------------------------------------------------
|
|
497
598
|
const CONSENT_YES = new Set(["y", "yes"]);
|
|
599
|
+
/** Fixed, value-free operator text for each bootstrap failure (BAPI-1288). */
|
|
600
|
+
const BOOTSTRAP_FAILURE_TEXT = Object.freeze({
|
|
601
|
+
"unsupported-protocol": "the Bridge API server does not support the gate-change preview protocol " +
|
|
602
|
+
"(it predates BAPI-1288) or answered a response this installer cannot verify. " +
|
|
603
|
+
"Upgrade the server before re-running; this installer never falls back to the " +
|
|
604
|
+
"older bootstrap that replaced configured gates.",
|
|
605
|
+
"approval-required": "the server requires an approved preview for this change. Re-run to preview it again.",
|
|
606
|
+
"stale-preview": "the project defaults changed since the preview, or the approval expired. " +
|
|
607
|
+
"Re-run to preview the current state again.",
|
|
608
|
+
"unknown-stored-state": "a stored project-default row cannot be read with certainty, so no change to it " +
|
|
609
|
+
"can be evaluated. See the account-settings operator runbook " +
|
|
610
|
+
"(unknown_stored_state) before re-running.",
|
|
611
|
+
"non-relaxable": "the change would turn auto-merge on or drop the human merge-approval requirement, " +
|
|
612
|
+
"which no gate-change approval can authorize.",
|
|
613
|
+
"invalid-proposal": "the server rejected the proposed configuration as unreadable.",
|
|
614
|
+
"state-unavailable": "the server could not read or lock the project defaults right now. Retry shortly.",
|
|
615
|
+
unauthorized: "the API key was rejected, or it is not an admin key for this repository.",
|
|
616
|
+
network: "the Bridge API could not be reached.",
|
|
617
|
+
timeout: "the Bridge API did not answer in time.",
|
|
618
|
+
server: "the Bridge API reported an internal error.",
|
|
619
|
+
rejected: "the Bridge API rejected the request.",
|
|
620
|
+
});
|
|
621
|
+
function reportBootstrapFailure(deps, stage, kind) {
|
|
622
|
+
const label = stage === "preview" ? "Bootstrap preview failed" : "Bootstrap failed";
|
|
623
|
+
deps.errorLog(`${label} (${kind}): ${BOOTSTRAP_FAILURE_TEXT[kind]}`);
|
|
624
|
+
deps.errorLog(stage === "preview"
|
|
625
|
+
? "No server write was attempted. No workflow or verification step was run."
|
|
626
|
+
: "Every configured requirement is unchanged. No workflow or verification step was run.");
|
|
627
|
+
}
|
|
628
|
+
/** Obligation tokens rendered as short fixed phrases — never server free text. */
|
|
629
|
+
const OBLIGATION_LABELS = Object.freeze({
|
|
630
|
+
ci_check_green: "required CI check green",
|
|
631
|
+
sticky_verdict_approved: "claude-review sticky verdict approved",
|
|
632
|
+
native_review_approved: "native GitHub review approved",
|
|
633
|
+
approval_minimum: "minimum approval count",
|
|
634
|
+
verdictless_review_parks: "verdictless review parks for a human",
|
|
635
|
+
});
|
|
636
|
+
function formatObligations(tokens) {
|
|
637
|
+
return tokens.length === 0 ? "none" : tokens.map((t) => OBLIGATION_LABELS[t] ?? t).join(", ");
|
|
638
|
+
}
|
|
639
|
+
function renderGateChangeEffect(deps, effect) {
|
|
640
|
+
deps.log(` - ${effect.row} ${effect.requirement}: ${effect.change}`);
|
|
641
|
+
deps.log(` ${effect.description}`);
|
|
642
|
+
deps.log(` removes: ${formatObligations(effect.removed_obligations)}`);
|
|
643
|
+
if (effect.replacement_obligations.length > 0) {
|
|
644
|
+
deps.log(` replaced by: ${formatObligations(effect.replacement_obligations)}`);
|
|
645
|
+
}
|
|
646
|
+
if (effect.removed_ci_check_count > 0) {
|
|
647
|
+
const refs = effect.removed_ci_check_refs.join(", ");
|
|
648
|
+
deps.log(` CI checks no longer required: ${effect.removed_ci_check_count}` +
|
|
649
|
+
(refs ? ` (refs: ${refs})` : ""));
|
|
650
|
+
}
|
|
651
|
+
if (effect.stored_min_approvals !== null || effect.proposed_min_approvals !== null) {
|
|
652
|
+
deps.log(` approval minimum: ${effect.stored_min_approvals ?? "none"} → ` +
|
|
653
|
+
`${effect.proposed_min_approvals ?? "none"}`);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
function renderBootstrapPreview(deps, preview) {
|
|
657
|
+
deps.log(" Server preview (nothing written yet):");
|
|
658
|
+
deps.log(` supervisor setup: ${preview.setup_outcome}`);
|
|
659
|
+
deps.log(` supervisor config: ${preview.config_outcome}`);
|
|
660
|
+
if (preview.changed_field_names.length > 0) {
|
|
661
|
+
deps.log(` fields that would change: ${preview.changed_field_names.join(", ")}`);
|
|
662
|
+
}
|
|
663
|
+
else {
|
|
664
|
+
deps.log(" nothing would change — the configured project defaults are already in place.");
|
|
665
|
+
}
|
|
666
|
+
if (preview.approval_required) {
|
|
667
|
+
deps.log("");
|
|
668
|
+
deps.log(" This change REMOVES or WEAKENS configured requirements:");
|
|
669
|
+
for (const effect of preview.effects)
|
|
670
|
+
renderGateChangeEffect(deps, effect);
|
|
671
|
+
deps.log(" CI check refs are opaque; compare them with GET /jira/epic-runs/supervisor-setup/defaults/.");
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
function renderBootstrapOutcome(deps, response) {
|
|
675
|
+
const describe = (outcome) => outcome === "preserved" ? "preserved (nothing written)" : String(outcome);
|
|
676
|
+
deps.log(` supervisor setup: ${describe(response.setup_outcome)}`);
|
|
677
|
+
deps.log(` supervisor config: ${describe(response.config_outcome)}`);
|
|
678
|
+
if (response.audited_field_names.length > 0) {
|
|
679
|
+
deps.log(` committed fields: ${response.audited_field_names.join(", ")}`);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
498
682
|
async function confirm(deps, promptText) {
|
|
499
683
|
// A missing prompt seam means no TTY. Silence is NOT consent.
|
|
500
684
|
if (!deps.promptLine)
|
|
@@ -760,6 +944,13 @@ export const CONDUCTOR_PROFILE_EMPTY_MERGE_REMEDIATION = `run \`npx ${MCP_PACKAG
|
|
|
760
944
|
/** Remediation printed whenever the managed conductor hooks were not installed. */
|
|
761
945
|
export const CONDUCTOR_HOOKS_MANUAL_REMEDIATION = "re-run `install conductor` and accept the local-observability consent, or run " +
|
|
762
946
|
"`epic-implementer install-git-hooks` directly.";
|
|
947
|
+
/**
|
|
948
|
+
* Remediation for a non-fatal hook refusal (BAPI-1290): the hooks need a
|
|
949
|
+
* persistent install to embed, and the repair is to create one and reinstall.
|
|
950
|
+
*/
|
|
951
|
+
export const CONDUCTOR_HOOKS_DURABLE_REMEDIATION = `install the CLI persistently (\`npm i -g ${MCP_PACKAGE_NAME}\`), then run ` +
|
|
952
|
+
"`epic-implementer install-git-hooks` (or re-run `install conductor` and accept the " +
|
|
953
|
+
"local-observability consent).";
|
|
763
954
|
/**
|
|
764
955
|
* Phase: tool visibility.
|
|
765
956
|
*
|
|
@@ -860,11 +1051,33 @@ async function runConductorLocalObservabilityPhase(deps, local) {
|
|
|
860
1051
|
// `reason` is already a sanitized sentence. This one IS a command failure —
|
|
861
1052
|
// a hook that cannot resolve its binary is a dead hook, and the whole point
|
|
862
1053
|
// of that ticket is that it no longer gets written silently.
|
|
1054
|
+
// BAPI-1290: the refusal names its fixed classification (e.g.
|
|
1055
|
+
// `ephemeral-cache` for an npx-cache runtime) or its per-hook planning
|
|
1056
|
+
// reasons. It is an OPTIONAL-hook installation refusal — distinct from a
|
|
1057
|
+
// setup-permission stop and from engine admission, which it never gates.
|
|
863
1058
|
deps.errorLog(result.error === CONDUCTOR_BIN_UNRESOLVED_ERROR
|
|
864
|
-
? ` hook installation refused — ${result.reason}`
|
|
865
|
-
:
|
|
866
|
-
|
|
867
|
-
|
|
1059
|
+
? ` hook installation refused (${result.classification}) — ${result.reason}`
|
|
1060
|
+
: ` hook installation refused — ${result.reason}`);
|
|
1061
|
+
deps.errorLog(" existing hooks were left unchanged; optional local hook delivery is unavailable " +
|
|
1062
|
+
"(engine admission is unaffected).");
|
|
1063
|
+
// Only BAPI-772's broken-package case stays fatal: the executing artifact
|
|
1064
|
+
// ships no hook binary at all. Every other refusal — an npx-cache runtime
|
|
1065
|
+
// (the documented `npx … install conductor` path), an unqualified or
|
|
1066
|
+
// unloadable runtime, an unsafe existing hook — is a manual action, and
|
|
1067
|
+
// the installer continues to setup-epic verification and executor
|
|
1068
|
+
// provisioning. Optional hooks must never stop the phases that do gate
|
|
1069
|
+
// a run.
|
|
1070
|
+
// A refusal without a classification (an older or injected result shape)
|
|
1071
|
+
// is read as `missing` — fail-closed to BAPI-772's fatal contract.
|
|
1072
|
+
const brokenPackage = result.error === CONDUCTOR_BIN_UNRESOLVED_ERROR &&
|
|
1073
|
+
(result.classification ?? "missing") === "missing";
|
|
1074
|
+
if (brokenPackage) {
|
|
1075
|
+
manualActions.push(`local observability: ${CONDUCTOR_HOOKS_MANUAL_REMEDIATION}`);
|
|
1076
|
+
fatal = true;
|
|
1077
|
+
}
|
|
1078
|
+
else {
|
|
1079
|
+
manualActions.push(`local observability: ${CONDUCTOR_HOOKS_DURABLE_REMEDIATION}`);
|
|
1080
|
+
}
|
|
868
1081
|
}
|
|
869
1082
|
else if (result !== null && result.ok) {
|
|
870
1083
|
if (!result.is_worktree) {
|
|
@@ -882,6 +1095,9 @@ async function runConductorLocalObservabilityPhase(deps, local) {
|
|
|
882
1095
|
? ` ${hook.name}: ${hook.action}`
|
|
883
1096
|
: ` ${hook.name}: ${hook.action} — warning: ${hook.warning}`);
|
|
884
1097
|
}
|
|
1098
|
+
if (result.partial) {
|
|
1099
|
+
deps.log(" the hook update is PARTIAL — one managed hook was published and the other was not.");
|
|
1100
|
+
}
|
|
885
1101
|
// Sanitized: the installer's own warnings are secret-free sentences, and
|
|
886
1102
|
// hook CONTENT, env, and caught errors are never rendered.
|
|
887
1103
|
for (const warning of result.warnings)
|
|
@@ -892,11 +1108,21 @@ async function runConductorLocalObservabilityPhase(deps, local) {
|
|
|
892
1108
|
// provisioned observability, and the missing reference-transaction hook
|
|
893
1109
|
// is precisely the one that stops half the events from ever being
|
|
894
1110
|
// emitted.
|
|
1111
|
+
//
|
|
1112
|
+
// BAPI-1290: each hook's `usable` is the installer's shared verdict —
|
|
1113
|
+
// published over a matching snapshot, executable, and backed by the
|
|
1114
|
+
// qualified persistent runtime — and the installer's own `complete`
|
|
1115
|
+
// flag must agree. Anything less is not recorded.
|
|
895
1116
|
const usable = (name) => {
|
|
896
1117
|
const hook = result.installed.find((h) => h.name === name);
|
|
897
|
-
return hook !== undefined &&
|
|
1118
|
+
return (hook !== undefined &&
|
|
1119
|
+
hook.usable === true &&
|
|
1120
|
+
hook.action !== "skipped" &&
|
|
1121
|
+
hook.warning === undefined);
|
|
898
1122
|
};
|
|
899
|
-
const complete =
|
|
1123
|
+
const complete = result.complete === true &&
|
|
1124
|
+
MANAGED_HOOK_NAMES.every((name) => usable(name)) &&
|
|
1125
|
+
result.warnings.length === 0;
|
|
900
1126
|
if (complete) {
|
|
901
1127
|
const recorded = await local.recordCapability("conductor_local_observability");
|
|
902
1128
|
if (!recorded.ok) {
|
|
@@ -948,6 +1174,277 @@ async function runConductorLocalObservabilityPhase(deps, local) {
|
|
|
948
1174
|
/**
|
|
949
1175
|
* Run the nested conductor installer. Returns the process exit code.
|
|
950
1176
|
*/
|
|
1177
|
+
// ---------------------------------------------------------------------------
|
|
1178
|
+
// Epic launch profile phase (BAPI-1260)
|
|
1179
|
+
// ---------------------------------------------------------------------------
|
|
1180
|
+
/** The packaged CLI's own npm spec, used when no absolute build path resolves. */
|
|
1181
|
+
const LAUNCH_NPM_CHANNEL_SPEC = `${MCP_PACKAGE_NAME}@latest`;
|
|
1182
|
+
/**
|
|
1183
|
+
* The stable invocation a profile records. A durable profile requires the
|
|
1184
|
+
* absolute packaged path (the same resolver the service unit uses, which already
|
|
1185
|
+
* refuses npx caches); a local one may fall back to the published package on
|
|
1186
|
+
* a named channel. Never a transient `_npx` cache path.
|
|
1187
|
+
*/
|
|
1188
|
+
function resolveLaunchRuntime(executor, mode) {
|
|
1189
|
+
const invocation = executor?.resolveInvocation();
|
|
1190
|
+
if (invocation?.ok) {
|
|
1191
|
+
const entryPath = invocation.invocation.prefixArgs[0];
|
|
1192
|
+
if (typeof entryPath === "string" && entryPath.length > 0) {
|
|
1193
|
+
return {
|
|
1194
|
+
ok: true,
|
|
1195
|
+
runtime: { kind: "packaged", executable: invocation.invocation.executable, entryPath },
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
if (mode === "durable") {
|
|
1200
|
+
return {
|
|
1201
|
+
ok: false,
|
|
1202
|
+
error: invocation && !invocation.ok
|
|
1203
|
+
? invocation.error
|
|
1204
|
+
: "no absolute packaged CLI path could be resolved for a durable launch.",
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
|
+
return { ok: true, runtime: { kind: "npm-channel", spec: LAUNCH_NPM_CHANNEL_SPEC } };
|
|
1208
|
+
}
|
|
1209
|
+
/** Loopback / link-local / private hostnames the API server cannot reach from elsewhere. */
|
|
1210
|
+
function isHostLocalHostname(hostname) {
|
|
1211
|
+
const h = hostname.replace(/^\[|\]$/g, "").toLowerCase();
|
|
1212
|
+
return (h === "localhost" ||
|
|
1213
|
+
h.endsWith(".localhost") ||
|
|
1214
|
+
h === "::1" ||
|
|
1215
|
+
/^127\./.test(h) ||
|
|
1216
|
+
/^0\.0\.0\.0$/.test(h) ||
|
|
1217
|
+
/^169\.254\./.test(h));
|
|
1218
|
+
}
|
|
1219
|
+
/**
|
|
1220
|
+
* Assess the policy's notification destination from the API SERVER's network
|
|
1221
|
+
* perspective. Presence and reachability SHAPE only — the authoritative
|
|
1222
|
+
* delivery check is the server's own `verify_notify_webhook` at approval, and a
|
|
1223
|
+
* failure there holds activation. A launcher-local listener can never pass
|
|
1224
|
+
* here for a remote target: the server cannot reach this machine's loopback.
|
|
1225
|
+
*/
|
|
1226
|
+
function assessNotification(policy, attended, targetBaseUrl, readiness) {
|
|
1227
|
+
if (attended) {
|
|
1228
|
+
return { status: "not-applicable", detail: "the referenced policy is attended; no escalation webhook is required." };
|
|
1229
|
+
}
|
|
1230
|
+
const notify = policy && typeof policy.notify === "object" && policy.notify !== null
|
|
1231
|
+
? policy.notify
|
|
1232
|
+
: null;
|
|
1233
|
+
const url = typeof notify?.webhook_url === "string" ? notify.webhook_url.trim() : "";
|
|
1234
|
+
if (url === "") {
|
|
1235
|
+
// BAPI-1274 — with no webhook declared, the account's email address is the
|
|
1236
|
+
// escalation channel when the server reports one is present (never the
|
|
1237
|
+
// address itself). A declared webhook stays authoritative and is assessed
|
|
1238
|
+
// below exactly as before; `notify.local_sink` still satisfies nothing.
|
|
1239
|
+
if (readiness?.unattended?.escalation_email_available === true) {
|
|
1240
|
+
return {
|
|
1241
|
+
status: "ok",
|
|
1242
|
+
detail: "No webhook declared; escalation alerts will be emailed to the account's email address",
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
return {
|
|
1246
|
+
status: "missing",
|
|
1247
|
+
detail: "an unattended launch needs notify.webhook_url in its --policy-file, or an email " +
|
|
1248
|
+
"address on the account so escalation alerts can be emailed" +
|
|
1249
|
+
(notify && "local_sink" in notify ? " (notify.local_sink is not an escalation channel)." : "."),
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1252
|
+
let webhook;
|
|
1253
|
+
let target;
|
|
1254
|
+
try {
|
|
1255
|
+
webhook = new URL(url);
|
|
1256
|
+
target = new URL(targetBaseUrl);
|
|
1257
|
+
}
|
|
1258
|
+
catch {
|
|
1259
|
+
return { status: "missing", detail: "notify.webhook_url is not an absolute URL." };
|
|
1260
|
+
}
|
|
1261
|
+
if (webhook.protocol !== "https:" && webhook.protocol !== "http:") {
|
|
1262
|
+
return { status: "missing", detail: "notify.webhook_url must be an http(s) URL." };
|
|
1263
|
+
}
|
|
1264
|
+
if (isHostLocalHostname(webhook.hostname) && !isHostLocalHostname(target.hostname)) {
|
|
1265
|
+
return {
|
|
1266
|
+
status: "missing",
|
|
1267
|
+
detail: "notify.webhook_url points at this machine's loopback, which the API server at the selected " +
|
|
1268
|
+
"target cannot reach. Use an API-reachable, durable notification service — a terminal-held " +
|
|
1269
|
+
"local listener cannot establish server reachability.",
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1272
|
+
return {
|
|
1273
|
+
status: "ok",
|
|
1274
|
+
detail: "notify.webhook_url is declared and addressable from the selected API server; the server " +
|
|
1275
|
+
"delivers a verification to it at approval, and a failure there holds activation.",
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1278
|
+
/**
|
|
1279
|
+
* Verify every mandatory launch prerequisite at the selected target, then —
|
|
1280
|
+
* and only then — persist the profile and print the one command that selects
|
|
1281
|
+
* it. Any unproved prerequisite yields `incomplete-setup` with the reason, and
|
|
1282
|
+
* no profile is written, so a profile's existence means setup was sufficient.
|
|
1283
|
+
*
|
|
1284
|
+
* Consent is READ from the server (`unattended.conductor_allowed`), never
|
|
1285
|
+
* written: ordinary configuration writes cannot self-grant unattended
|
|
1286
|
+
* authority, and nothing in this phase tries to.
|
|
1287
|
+
*/
|
|
1288
|
+
async function runLaunchProfilePhase(deps, options, inputs) {
|
|
1289
|
+
const name = options.launchProfile;
|
|
1290
|
+
const mode = options.executionMode ?? "local";
|
|
1291
|
+
const lp = deps.launchProfiles ?? createDefaultLaunchProfileDeps(deps.env);
|
|
1292
|
+
const repoName = inputs.access.repoName;
|
|
1293
|
+
const gaps = [];
|
|
1294
|
+
deps.log("");
|
|
1295
|
+
deps.log(`Epic launch profile '${name}' — verification…`);
|
|
1296
|
+
deps.log(` target: ${inputs.baseUrl}`);
|
|
1297
|
+
deps.log(` repository: ${repoName}`);
|
|
1298
|
+
deps.log(` mode: ${mode}`);
|
|
1299
|
+
// (1) Access at THIS target — only a server answer proves the key works there.
|
|
1300
|
+
if (inputs.readiness === null) {
|
|
1301
|
+
gaps.push("the selected target's conductor readiness could not be read with this repository's credential.");
|
|
1302
|
+
}
|
|
1303
|
+
// (2) The policy reference and what it implies.
|
|
1304
|
+
let policy = null;
|
|
1305
|
+
let policyPath = null;
|
|
1306
|
+
if (options.policyFile !== undefined) {
|
|
1307
|
+
policyPath = path.resolve(deps.cwd, options.policyFile);
|
|
1308
|
+
try {
|
|
1309
|
+
const parsed = JSON.parse(await lp.readPolicyFile(policyPath));
|
|
1310
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
1311
|
+
policy = parsed;
|
|
1312
|
+
}
|
|
1313
|
+
else {
|
|
1314
|
+
gaps.push("the --policy-file does not hold a JSON object.");
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
catch {
|
|
1318
|
+
gaps.push("the --policy-file could not be read as JSON.");
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
const attended = policy?.posture === "attended";
|
|
1322
|
+
// (3) Consent — the server's administrative authority, read not written.
|
|
1323
|
+
const consent = inputs.readiness?.unattended?.conductor_allowed;
|
|
1324
|
+
if (attended) {
|
|
1325
|
+
deps.log(" consent: not applicable (attended policy)");
|
|
1326
|
+
}
|
|
1327
|
+
else if (consent === true) {
|
|
1328
|
+
deps.log(" consent: confirmed by the server (unattended_epic_implementer_allowed)");
|
|
1329
|
+
}
|
|
1330
|
+
else {
|
|
1331
|
+
deps.log(` consent: ${consent === false ? "NOT granted" : "could not be verified"}`);
|
|
1332
|
+
gaps.push(consent === false
|
|
1333
|
+
? "unattended consent is off for this repository; turn on 'Allow unattended Epic " +
|
|
1334
|
+
"Implementer runs' on the repository's Setup page in the Bridge admin panel " +
|
|
1335
|
+
"(if it is already on, contact Bridge support) — this command never writes it."
|
|
1336
|
+
: "unattended consent could not be verified at the selected target (older server or unreadable report).");
|
|
1337
|
+
}
|
|
1338
|
+
// (4) Notification, from the server's network perspective.
|
|
1339
|
+
const notification = assessNotification(policy, attended, inputs.baseUrl, inputs.readiness);
|
|
1340
|
+
deps.log(` notify: ${notification.status} — ${notification.detail}`);
|
|
1341
|
+
if (notification.status === "missing")
|
|
1342
|
+
gaps.push(notification.detail);
|
|
1343
|
+
// (5) The actor that consumes the work.
|
|
1344
|
+
if (mode === "durable") {
|
|
1345
|
+
deps.log(` actor: ${inputs.actorObserved ? "server-observed executor handshake" : "NOT observed"}`);
|
|
1346
|
+
if (!inputs.actorObserved) {
|
|
1347
|
+
gaps.push("no executor was observed by the server for this repository; a written unit, a started " +
|
|
1348
|
+
"service, or a local process is not proof. Provision it with --executor-id and let the " +
|
|
1349
|
+
"bounded observation complete.");
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
else {
|
|
1353
|
+
deps.log(" actor: local — drive-epic starts this repository's plane and scoped lanes itself");
|
|
1354
|
+
}
|
|
1355
|
+
// (6) A stable runtime invocation.
|
|
1356
|
+
if (!inputs.runtime.ok) {
|
|
1357
|
+
gaps.push(`the packaged CLI invocation is not stable: ${inputs.runtime.error}`);
|
|
1358
|
+
}
|
|
1359
|
+
else {
|
|
1360
|
+
deps.log(` runtime: ${inputs.runtime.runtime.kind === "packaged" ? "packaged absolute path" : inputs.runtime.runtime.spec}`);
|
|
1361
|
+
}
|
|
1362
|
+
// (7) The existing profile: a changed binding is re-approved explicitly.
|
|
1363
|
+
const existing = await lp.readProfile(name);
|
|
1364
|
+
if (existing.ok) {
|
|
1365
|
+
const prior = existing.profile;
|
|
1366
|
+
const changed = prior.baseUrl !== inputs.baseUrl || prior.repoName !== repoName || prior.executionMode !== mode;
|
|
1367
|
+
if (changed) {
|
|
1368
|
+
deps.log("");
|
|
1369
|
+
deps.log(` profile '${name}' exists with a DIFFERENT binding:`);
|
|
1370
|
+
deps.log(` current: ${prior.baseUrl} repo=${prior.repoName} mode=${prior.executionMode}`);
|
|
1371
|
+
deps.log(` proposed: ${inputs.baseUrl} repo=${repoName} mode=${mode}`);
|
|
1372
|
+
const rebind = await confirm(deps, `Re-bind profile '${name}' to the proposed target? [y/N]: `);
|
|
1373
|
+
if (!rebind)
|
|
1374
|
+
gaps.push(`re-binding profile '${name}' was declined; it was left unchanged.`);
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
else if (existing.kind !== "profile-missing") {
|
|
1378
|
+
// Unreadable or from a newer/older schema: reported, and replaced only
|
|
1379
|
+
// through the same consented write below.
|
|
1380
|
+
deps.log(` existing profile: ${existing.message}`);
|
|
1381
|
+
}
|
|
1382
|
+
// (8) The credential binding — the explicit one-time upgrade path.
|
|
1383
|
+
if (gaps.length === 0) {
|
|
1384
|
+
const bound = await lp.readCredentialBinding(repoName);
|
|
1385
|
+
if (bound === inputs.baseUrl) {
|
|
1386
|
+
deps.log(` credential: bapi:${repoName} is already approved for this target`);
|
|
1387
|
+
}
|
|
1388
|
+
else {
|
|
1389
|
+
deps.log(bound === null
|
|
1390
|
+
? ` credential: bapi:${repoName} is not yet approved for any endpoint (legacy repo-only binding)`
|
|
1391
|
+
: ` credential: bapi:${repoName} is approved for a DIFFERENT endpoint`);
|
|
1392
|
+
const approve = await confirm(deps, `Approve the stored credential bapi:${repoName} for ${inputs.baseUrl} only? [y/N]: `);
|
|
1393
|
+
if (!approve) {
|
|
1394
|
+
gaps.push("the credential binding was not approved, so a launch could not use the stored key.");
|
|
1395
|
+
}
|
|
1396
|
+
else {
|
|
1397
|
+
const result = await lp.bindCredential(repoName, inputs.baseUrl);
|
|
1398
|
+
if (!result.ok)
|
|
1399
|
+
gaps.push(`the credential binding could not be written: ${result.error}`);
|
|
1400
|
+
else
|
|
1401
|
+
deps.log(` credential: ${result.action} — approved for this target only`);
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
if (gaps.length > 0 || !inputs.runtime.ok) {
|
|
1406
|
+
deps.log("");
|
|
1407
|
+
deps.log(` incomplete — profile '${name}' was NOT written:`);
|
|
1408
|
+
for (const gap of gaps)
|
|
1409
|
+
deps.log(` → ${gap}`);
|
|
1410
|
+
deps.log(` re-run: npx -y ${MCP_PACKAGE_NAME} install conductor --launch-profile ${name} ` +
|
|
1411
|
+
`--base-url ${inputs.baseUrl} --required-check <name> ...`);
|
|
1412
|
+
return { complete: false };
|
|
1413
|
+
}
|
|
1414
|
+
const write = await confirm(deps, `Write epic launch profile '${name}' (references only, no secrets)? [y/N]: `);
|
|
1415
|
+
if (!write) {
|
|
1416
|
+
deps.log(" declined — no profile was written.");
|
|
1417
|
+
return { complete: false };
|
|
1418
|
+
}
|
|
1419
|
+
const profile = {
|
|
1420
|
+
version: EPIC_LAUNCH_PROFILE_VERSION,
|
|
1421
|
+
name,
|
|
1422
|
+
baseUrl: inputs.baseUrl,
|
|
1423
|
+
repoName,
|
|
1424
|
+
executionMode: mode,
|
|
1425
|
+
host: lp.hostname(),
|
|
1426
|
+
runtime: inputs.runtime.runtime,
|
|
1427
|
+
policyRef: policyPath !== null ? { kind: "file", path: policyPath } : null,
|
|
1428
|
+
credentialRef: { store: "bridge-credentials", target: `bapi:${repoName}` },
|
|
1429
|
+
approvedAt: lp.nowIso(),
|
|
1430
|
+
};
|
|
1431
|
+
const written = await lp.writeProfile(profile);
|
|
1432
|
+
if (!written.ok) {
|
|
1433
|
+
deps.errorLog(` profile write failed: ${written.message}`);
|
|
1434
|
+
return { complete: false };
|
|
1435
|
+
}
|
|
1436
|
+
deps.log(` ${written.action} ${written.path}`);
|
|
1437
|
+
deps.log("");
|
|
1438
|
+
deps.log("Launch command (fresh shell; no exports needed):");
|
|
1439
|
+
deps.log(` ${renderEpicLaunchCommand(profile, {
|
|
1440
|
+
epicKey: options.epicKey ?? "<EPIC>",
|
|
1441
|
+
planFile: options.planFile ? path.resolve(deps.cwd, options.planFile) : "<path>",
|
|
1442
|
+
})
|
|
1443
|
+
// Placeholders are for the operator to replace, not literal arguments.
|
|
1444
|
+
.replace("'<EPIC>'", "<EPIC>")
|
|
1445
|
+
.replace("'<path>'", "<path>")}`);
|
|
1446
|
+
return { complete: true };
|
|
1447
|
+
}
|
|
951
1448
|
export async function runInstallBridgeConductorCli(argv, deps) {
|
|
952
1449
|
const parsed = parseInstallBridgeConductorArgs(argv);
|
|
953
1450
|
if (parsed.status === "help") {
|
|
@@ -962,12 +1459,31 @@ export async function runInstallBridgeConductorCli(argv, deps) {
|
|
|
962
1459
|
}
|
|
963
1460
|
const options = parsed.options;
|
|
964
1461
|
// ---- Base URL: normalized ONCE, confirmed BEFORE any network call ------
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
1462
|
+
// BAPI-1260: an epic launch profile has NO production default. Its target is
|
|
1463
|
+
// --base-url or BAPI_BASE_URL (they must agree), decided here before any
|
|
1464
|
+
// request; the hosted default below applies to the legacy flow only.
|
|
1465
|
+
let baseUrl;
|
|
1466
|
+
if (options.launchProfile !== undefined) {
|
|
1467
|
+
const target = resolveEpicLaunchTarget({
|
|
1468
|
+
flagBaseUrl: options.baseUrl,
|
|
1469
|
+
profile: null,
|
|
1470
|
+
env: deps.env,
|
|
1471
|
+
});
|
|
1472
|
+
if (!target.ok) {
|
|
1473
|
+
deps.errorLog(target.message);
|
|
1474
|
+
deps.errorLog("Nothing was sent anywhere and nothing was written.");
|
|
1475
|
+
return 1;
|
|
1476
|
+
}
|
|
1477
|
+
baseUrl = target.baseUrl;
|
|
1478
|
+
}
|
|
1479
|
+
else {
|
|
1480
|
+
const baseUrlResult = deps.resolveBaseUrl(deps.env);
|
|
1481
|
+
if (!baseUrlResult.ok) {
|
|
1482
|
+
deps.errorLog(baseUrlResult.error);
|
|
1483
|
+
return 1;
|
|
1484
|
+
}
|
|
1485
|
+
baseUrl = baseUrlResult.value;
|
|
969
1486
|
}
|
|
970
|
-
const baseUrl = baseUrlResult.value;
|
|
971
1487
|
// ---- Consent #1: the resolved base URL ---------------------------------
|
|
972
1488
|
// This consent gates the FIRST network request of any kind — including the
|
|
973
1489
|
// read-only doctor readiness GET (which carries this repository's API key)
|
|
@@ -975,6 +1491,10 @@ export async function runInstallBridgeConductorCli(argv, deps) {
|
|
|
975
1491
|
// BAPI_BASE_URL is unset, so the operator must see and confirm the target
|
|
976
1492
|
// before the key is sent anywhere. No TTY means no consent means abort.
|
|
977
1493
|
deps.log(`Bridge API base URL: ${baseUrl}`);
|
|
1494
|
+
if (options.launchProfile !== undefined) {
|
|
1495
|
+
deps.log(` explicit target for epic launch profile '${options.launchProfile}'` +
|
|
1496
|
+
(baseUrl === DEFAULT_BAPI_BASE_URL ? " — this is the PRODUCTION Bridge API, selected deliberately." : "."));
|
|
1497
|
+
}
|
|
978
1498
|
const baseUrlOk = await confirm(deps, "Bootstrap against this URL? [y/N]: ");
|
|
979
1499
|
if (!baseUrlOk) {
|
|
980
1500
|
deps.log("Aborted — no network call (readiness GET or bootstrap POST) was attempted.");
|
|
@@ -1059,16 +1579,19 @@ export async function runInstallBridgeConductorCli(argv, deps) {
|
|
|
1059
1579
|
if (options.dryRun) {
|
|
1060
1580
|
deps.log("");
|
|
1061
1581
|
deps.log("Dry run — no writes will be performed.");
|
|
1062
|
-
deps.log(" Would POST /jira/epic-runs/supervisor-bootstrap with field NAMES:");
|
|
1582
|
+
deps.log(" Would PREVIEW, then POST /jira/epic-runs/supervisor-bootstrap with field NAMES:");
|
|
1063
1583
|
// Field NAMES and a COUNT only — never the check values, which are
|
|
1064
1584
|
// configuration content, and never the API key.
|
|
1065
1585
|
deps.log(" - done_gate_config.required_checks " +
|
|
1066
1586
|
`(${options.requiredChecks.length} check(s))`);
|
|
1067
|
-
deps.log(" - review_source");
|
|
1068
1587
|
if (options.reviewPolicyExplicit)
|
|
1069
1588
|
deps.log(" - review_policy");
|
|
1589
|
+
if (options.replaceDoneGate)
|
|
1590
|
+
deps.log(" - replace_done_gate");
|
|
1070
1591
|
deps.log(" - auto_merge_enabled");
|
|
1071
1592
|
deps.log(" - merge_approval_required");
|
|
1593
|
+
deps.log(" Existing project defaults would be KEPT: missing checks are added, nothing is " +
|
|
1594
|
+
"removed without a previewed, separately approved change.");
|
|
1072
1595
|
deps.log(` Would target base URL: ${baseUrl}`);
|
|
1073
1596
|
deps.log(options.skipWorkflow || options.reviewPolicy !== "verdict_protocol"
|
|
1074
1597
|
? " Workflow scaffold: skipped (not applicable for this policy)"
|
|
@@ -1101,11 +1624,50 @@ export async function runInstallBridgeConductorCli(argv, deps) {
|
|
|
1101
1624
|
else if (executor) {
|
|
1102
1625
|
deps.log(" Executor provisioning: skipped (no --executor-id supplied)");
|
|
1103
1626
|
}
|
|
1627
|
+
if (options.launchProfile !== undefined) {
|
|
1628
|
+
deps.log(` Epic launch profile '${options.launchProfile}': would verify target access, credential ` +
|
|
1629
|
+
"binding, consent, notification, and actor, and persist the profile only if all hold " +
|
|
1630
|
+
"(each write behind its own consent) — nothing was read from or written to the profile store.");
|
|
1631
|
+
}
|
|
1104
1632
|
renderCapabilityMatrix(deps, firstReport.readiness);
|
|
1105
1633
|
deps.log("");
|
|
1106
1634
|
deps.log("complete-with-manual-steps (dry run)");
|
|
1107
1635
|
return 0;
|
|
1108
1636
|
}
|
|
1637
|
+
if (!access) {
|
|
1638
|
+
// Unreachable in practice (a null access produces a fatal doctor section
|
|
1639
|
+
// above), but the type must be narrowed before the preview.
|
|
1640
|
+
deps.errorLog("Bridge API access could not be resolved; aborting before the write.");
|
|
1641
|
+
return 1;
|
|
1642
|
+
}
|
|
1643
|
+
// ---- Phase 2: bootstrap — preview, consent, exact approval, apply -------
|
|
1644
|
+
// BAPI-1288. The server decides what this bootstrap would do to the stored
|
|
1645
|
+
// project defaults; this command only shows that decision and asks. It never
|
|
1646
|
+
// evaluates gate strength itself, and it never falls back to the old
|
|
1647
|
+
// replacing bootstrap when the server cannot preview.
|
|
1648
|
+
deps.log("");
|
|
1649
|
+
deps.log("Phase 2/8 — bootstrapping supervisor project defaults…");
|
|
1650
|
+
const bootstrapRequest = {
|
|
1651
|
+
required_checks: options.requiredChecks,
|
|
1652
|
+
// BAPI-694 — the narrow ReviewPolicy object, sent ONLY on an explicit
|
|
1653
|
+
// selection. `review_source` is deliberately NOT sent: on an existing
|
|
1654
|
+
// repository an omitted field keeps its stored value, and a new repository
|
|
1655
|
+
// receives the server's `verdict_protocol` default either way.
|
|
1656
|
+
...(options.reviewPolicyExplicit
|
|
1657
|
+
? { review_policy: { source: options.reviewPolicy } }
|
|
1658
|
+
: {}),
|
|
1659
|
+
merge_approval_required: "true",
|
|
1660
|
+
...(options.replaceDoneGate ? { replace_done_gate: true } : {}),
|
|
1661
|
+
};
|
|
1662
|
+
let preview;
|
|
1663
|
+
try {
|
|
1664
|
+
preview = await previewConductorSupervisorBootstrap(access, bootstrapRequest, deps.postJson);
|
|
1665
|
+
}
|
|
1666
|
+
catch (err) {
|
|
1667
|
+
reportBootstrapFailure(deps, "preview", classifySupervisorBootstrapFailure(err));
|
|
1668
|
+
return 1;
|
|
1669
|
+
}
|
|
1670
|
+
renderBootstrapPreview(deps, preview);
|
|
1109
1671
|
// ---- Consent #2: the bootstrap write itself ----------------------------
|
|
1110
1672
|
// Deliberately separate from the base-URL consent above (which was given
|
|
1111
1673
|
// before any network call). Confirming WHERE to write is not consent TO write.
|
|
@@ -1115,36 +1677,36 @@ export async function runInstallBridgeConductorCli(argv, deps) {
|
|
|
1115
1677
|
deps.log("Aborted — no server write was attempted.");
|
|
1116
1678
|
return 0;
|
|
1117
1679
|
}
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1680
|
+
// ---- Consent #3: the EXACT gate change, only when the preview needs it ---
|
|
1681
|
+
// Never inferred from consent #2: agreeing to install is not agreeing to
|
|
1682
|
+
// remove a configured requirement. A missing prompt seam is a refusal.
|
|
1683
|
+
let applyRequest = bootstrapRequest;
|
|
1684
|
+
if (preview.approval_required) {
|
|
1685
|
+
deps.log("");
|
|
1686
|
+
const gateChangeOk = await confirm(deps, "Approve EXACTLY the gate changes listed above? [y/N]: ");
|
|
1687
|
+
if (!gateChangeOk || !preview.approval_token) {
|
|
1688
|
+
deps.errorLog("Gate change not approved — no server write was attempted, and every configured " +
|
|
1689
|
+
"requirement is unchanged.");
|
|
1690
|
+
return 1;
|
|
1691
|
+
}
|
|
1692
|
+
applyRequest = {
|
|
1693
|
+
...bootstrapRequest,
|
|
1694
|
+
approval: { approve: true, approval_token: preview.approval_token },
|
|
1695
|
+
};
|
|
1123
1696
|
}
|
|
1124
|
-
// ---- Phase 2: bootstrap ------------------------------------------------
|
|
1125
|
-
deps.log("");
|
|
1126
|
-
deps.log("Phase 2/8 — bootstrapping supervisor project defaults…");
|
|
1127
1697
|
let bootstrapResponse;
|
|
1128
1698
|
try {
|
|
1129
|
-
bootstrapResponse = await bootstrapConductorSupervisorDefaults(access,
|
|
1130
|
-
required_checks: options.requiredChecks,
|
|
1131
|
-
review_source: "verdict_protocol",
|
|
1132
|
-
// BAPI-694 — the narrow ReviewPolicy object, sent ONLY on an explicit
|
|
1133
|
-
// selection. `review_source` above is deliberately left alone: it is the
|
|
1134
|
-
// WIDER done-gate vocabulary and the two are not interchangeable, so
|
|
1135
|
-
// neither value is coerced into the other.
|
|
1136
|
-
...(options.reviewPolicyExplicit
|
|
1137
|
-
? { review_policy: { source: options.reviewPolicy } }
|
|
1138
|
-
: {}),
|
|
1139
|
-
merge_approval_required: "true",
|
|
1140
|
-
}, deps.postJson);
|
|
1699
|
+
bootstrapResponse = await bootstrapConductorSupervisorDefaults(access, applyRequest, deps.postJson);
|
|
1141
1700
|
}
|
|
1142
1701
|
catch (err) {
|
|
1143
|
-
|
|
1144
|
-
deps.errorLog(`Bootstrap failed (${kind}). No workflow or verification step was run.`);
|
|
1702
|
+
reportBootstrapFailure(deps, "apply", classifySupervisorBootstrapFailure(err));
|
|
1145
1703
|
return 1;
|
|
1146
1704
|
}
|
|
1147
|
-
|
|
1705
|
+
if (bootstrapResponse.setup_outcome === null || bootstrapResponse.config_outcome === null) {
|
|
1706
|
+
reportBootstrapFailure(deps, "apply", "unsupported-protocol");
|
|
1707
|
+
return 1;
|
|
1708
|
+
}
|
|
1709
|
+
renderBootstrapOutcome(deps, bootstrapResponse);
|
|
1148
1710
|
// ---- Phase 3: optional workflow scaffold -------------------------------
|
|
1149
1711
|
deps.log("");
|
|
1150
1712
|
deps.log("Phase 3/8 — claude-review workflow…");
|
|
@@ -1235,8 +1797,10 @@ export async function runInstallBridgeConductorCli(argv, deps) {
|
|
|
1235
1797
|
manualActions.push("local observability: run the packaged `install conductor` command, or " +
|
|
1236
1798
|
"`epic-implementer install-git-hooks`, to install the managed conductor Git hooks.");
|
|
1237
1799
|
}
|
|
1238
|
-
// The fail-loud hook-binary refusal
|
|
1239
|
-
// read-only ledger report has been
|
|
1800
|
+
// The fail-loud hook-binary refusal (a package that ships no hook binary)
|
|
1801
|
+
// exits 1 — but only AFTER the phase's read-only ledger report has been
|
|
1802
|
+
// printed, which the phase guarantees. BAPI-1290: qualification refusals are
|
|
1803
|
+
// manual actions, not fatal, so they reach the phases below.
|
|
1240
1804
|
if (observabilityFatal)
|
|
1241
1805
|
return 1;
|
|
1242
1806
|
// ---- Phase 6: setup-epic verification (conditional) --------------------
|
|
@@ -1320,6 +1884,24 @@ export async function runInstallBridgeConductorCli(argv, deps) {
|
|
|
1320
1884
|
for (const action of manualActions)
|
|
1321
1885
|
deps.log(` → ${action}`);
|
|
1322
1886
|
}
|
|
1887
|
+
// ---- BAPI-1260: the reusable epic launch profile ------------------------
|
|
1888
|
+
// Last, because it can only be approved on facts every earlier phase
|
|
1889
|
+
// produced: server-observed readiness at THIS target, the executor handshake
|
|
1890
|
+
// for a durable host, and the operator's consents.
|
|
1891
|
+
if (options.launchProfile !== undefined) {
|
|
1892
|
+
const launch = await runLaunchProfilePhase(deps, options, {
|
|
1893
|
+
access,
|
|
1894
|
+
baseUrl,
|
|
1895
|
+
readiness: postReadiness,
|
|
1896
|
+
actorObserved: observedReadiness !== null || postReadiness?.executor.ready === true,
|
|
1897
|
+
runtime: resolveLaunchRuntime(executor, options.executionMode ?? "local"),
|
|
1898
|
+
});
|
|
1899
|
+
deps.log("");
|
|
1900
|
+
if (!launch.complete) {
|
|
1901
|
+
deps.log("incomplete-setup");
|
|
1902
|
+
return 1;
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1323
1905
|
deps.log("");
|
|
1324
1906
|
// The executor leg can only be true when the server observed a recent claim,
|
|
1325
1907
|
// so a bootstrap alone can never print `complete`. The two local phases above
|