@bridge_gpt/mcp-server 0.2.62 → 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 +66 -15
- package/build/agent-launchers/claude-executor-adapter.js +145 -20
- package/build/agent-launchers/executor-adapter.js +81 -2
- package/build/agents.generated.js +10 -1
- package/build/bounded-wait.js +32 -1
- package/build/bridge-config.js +104 -0
- package/build/codex-bridge-profile.js +10 -1
- package/build/commands.generated.js +7 -6
- package/build/conductor/bridge-api-client.js +565 -32
- package/build/conductor/bring-up-facts.js +3 -1
- package/build/conductor/doctor.js +23 -14
- package/build/conductor/epic-child-continuation.js +351 -0
- package/build/conductor/epic-implementer-cli.js +154 -17
- package/build/conductor/git-hooks.js +529 -118
- package/build/conductor/guided-repair.js +323 -0
- package/build/conductor/hook-runtime-qualification.js +340 -0
- package/build/conductor/readiness-cli.js +30 -9
- package/build/conductor/readiness-sections.js +5 -4
- package/build/conductor/readiness.js +45 -12
- package/build/conductor/recovery-cli.js +859 -27
- package/build/conductor/recovery-operations.js +574 -5
- package/build/conductor/repair-contract.js +69 -3
- package/build/conductor-bundle-artifacts.js +1 -0
- package/build/credential-store.js +52 -13
- package/build/diagnostic-text.js +36 -0
- package/build/docs.generated.js +3 -2
- package/build/doctor.js +147 -21
- package/build/drive-epic-key.js +12 -0
- package/build/drive-epic.js +1537 -125
- 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 +41 -33
- package/build/epic-implementer-bundle-cli.js +14 -1
- package/build/epic-launch-diagnostic.js +31 -0
- package/build/epic-launch-profile.js +58 -19
- package/build/epic-run-status.js +5 -0
- package/build/epic-shared/bridge-client.js +46 -4
- package/build/executor/cli.js +39 -0
- package/build/executor/completion-contract.js +423 -0
- package/build/executor/completion-supervision.js +291 -0
- package/build/executor/correction-dispatch.js +60 -0
- package/build/executor/credentials.js +3 -1
- package/build/executor/deps.js +65 -1
- package/build/executor/env.js +12 -0
- package/build/executor/heartbeat.js +13 -3
- package/build/executor/http-client.js +91 -4
- package/build/executor/integration-job.js +6 -1
- package/build/executor/job-errors.js +61 -4
- package/build/executor/job-log-registry.js +340 -73
- package/build/executor/job-runner.js +1550 -55
- package/build/executor/job-types.js +9 -6
- package/build/executor/preflight.js +189 -1
- package/build/executor/process-containment.js +756 -0
- package/build/executor/process.js +235 -5
- package/build/executor/prompt-spec.js +366 -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 +168 -0
- package/build/executor/stale-artifacts.js +5 -4
- package/build/executor/test-declaration.js +20 -0
- package/build/executor/types.js +13 -0
- package/build/executor/watch-cli.js +35 -8
- package/build/executor/worker-command.js +70 -34
- package/build/executor/worker-finalization.js +168 -26
- package/build/executor/worker-guard-hook.js +75 -14
- package/build/executor/worker-log.js +137 -70
- package/build/executor/worktree-gc.js +30 -6
- package/build/executor/worktree.js +13 -0
- package/build/executor-launch.js +658 -0
- package/build/index.js +594 -1152
- package/build/init.js +107 -4
- package/build/install-bridge-conductor.js +231 -41
- 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/merge-pull-request.js +1 -1
- package/build/model-thresholds-tool.js +185 -0
- 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/status.js +4 -0
- package/build/plane/supervisor.js +146 -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/scripts/executor-protocol-contract-driver.js +13 -3
- package/build/setup-epic.js +208 -125
- 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 +5 -4
- package/build/worktree-core.js +6 -5
- package/docs/CONDUCTOR.md +116 -2
- package/docs/install/mcp-tool-integrations.md +1 -0
- package/docs/install/sfcc-integration.md +12 -7
- package/package.json +2 -1
- package/pipelines/implement-ticket.json +1 -1
package/build/init.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* effects and depends only on leaf modules.
|
|
11
11
|
*/
|
|
12
12
|
import { writeFile, mkdir, readFile, stat } from "fs/promises";
|
|
13
|
+
import { execFile } from "child_process";
|
|
13
14
|
import path from "path";
|
|
14
15
|
import os from "os";
|
|
15
16
|
import { COMMANDS } from "./commands.generated.js";
|
|
@@ -19,7 +20,7 @@ import { AGENTS } from "./agents.generated.js";
|
|
|
19
20
|
import { DOCS } from "./docs.generated.js";
|
|
20
21
|
import { VERSION, LAUNCHER_ARGS } from "./version.generated.js";
|
|
21
22
|
import { reconstructAgentMarkdown, translateAgentToCopilot, translateCommandToCopilot, } from "./agent-utils.js";
|
|
22
|
-
import { validateRepoName, resolveRepoNameForProjectRoot } from "./bridge-config.js";
|
|
23
|
+
import { BRIDGE_CONFIG_TEAMMATE_NOTICE_LINES, validateBridgeConfigRepoNameRepresentability, validateRepoName, resolveRepoNameForProjectRoot, } from "./bridge-config.js";
|
|
23
24
|
import { ensureGitignored as ensureGitignoredShared } from "./git-ignore-utils.js";
|
|
24
25
|
import { resolveBapiCredentials } from "./credential-store.js";
|
|
25
26
|
import { describeLauncherReason, DUPLICATE_REGISTRATION_GUIDANCE, inspectLauncherConfigs, isPrerequisiteFailure, } from "./launcher-config-inspection.js";
|
|
@@ -133,6 +134,8 @@ export function buildBridgeConfigManifest(repoName) {
|
|
|
133
134
|
"# Inherited by every git worktree; start-tickets worktree provisioning reads it",
|
|
134
135
|
"# to decide which Bridge API MCP registrations to write. Do not add credentials,",
|
|
135
136
|
"# connection URLs, or machine-specific paths here — those are resolved at runtime.",
|
|
137
|
+
"#",
|
|
138
|
+
...BRIDGE_CONFIG_TEAMMATE_NOTICE_LINES,
|
|
136
139
|
"",
|
|
137
140
|
`repo_name = "${repoName}"`,
|
|
138
141
|
"",
|
|
@@ -389,6 +392,65 @@ export class InitPreflightError extends Error {
|
|
|
389
392
|
this.failures = failures;
|
|
390
393
|
}
|
|
391
394
|
}
|
|
395
|
+
/**
|
|
396
|
+
* Classify `git ls-files --error-unmatch -- <relPath>` from its exit code: 0 is
|
|
397
|
+
* tracked, 1 is untracked, anything else is undetermined. Shared by `runInit`
|
|
398
|
+
* and install so both reach the same verdict from the same probe.
|
|
399
|
+
*/
|
|
400
|
+
export function classifyTrackedProbeExitCode(exitCode) {
|
|
401
|
+
if (exitCode === 0)
|
|
402
|
+
return "tracked";
|
|
403
|
+
if (exitCode === 1)
|
|
404
|
+
return "untracked";
|
|
405
|
+
return "undetermined";
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Default tracked-config probe for `runInit`: runs git with an ARGUMENT LIST,
|
|
409
|
+
* never a shell string, in `cwd`. Any failure is `undetermined`, which callers
|
|
410
|
+
* treat as untracked — the probe fails OPEN, so a missing git or a non-repo
|
|
411
|
+
* folder still gets today's `BAPI_PROJECT_ROOT = cwd` rewrite.
|
|
412
|
+
*/
|
|
413
|
+
export function defaultIsTrackedConfig(cwd) {
|
|
414
|
+
return (relPath) => new Promise((resolve) => {
|
|
415
|
+
try {
|
|
416
|
+
execFile("git", ["ls-files", "--error-unmatch", "--", relPath], { cwd, windowsHide: true, timeout: 10_000 }, (error) => {
|
|
417
|
+
if (!error)
|
|
418
|
+
return resolve("tracked");
|
|
419
|
+
const code = error.code;
|
|
420
|
+
resolve(classifyTrackedProbeExitCode(typeof code === "number" ? code : null));
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
catch {
|
|
424
|
+
resolve("undetermined");
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Decide the `BAPI_PROJECT_ROOT` an EXISTING Bridge entry should carry (BAPI-1318).
|
|
430
|
+
* A git-tracked config keeps its existing non-blank value exactly: the runtime
|
|
431
|
+
* check in `project-root.ts` makes a committed path harmless for teammates, and
|
|
432
|
+
* rewriting it would put this machine's path into everyone's diff. Untracked and
|
|
433
|
+
* undetermined configs, and blank or absent values, get `cwd` as before.
|
|
434
|
+
*
|
|
435
|
+
* `warnReplaced` is true only when the probe was undetermined AND a different,
|
|
436
|
+
* non-blank value is being replaced — the one case where a tracked path may have
|
|
437
|
+
* been rewritten without the protection applying.
|
|
438
|
+
*/
|
|
439
|
+
export function decideExistingProjectRoot(existing, cwd, tracked) {
|
|
440
|
+
const current = typeof existing === "string" ? existing : "";
|
|
441
|
+
const blank = current.trim().length === 0;
|
|
442
|
+
if (tracked === "tracked" && !blank)
|
|
443
|
+
return { value: current, warnReplaced: false };
|
|
444
|
+
return {
|
|
445
|
+
value: cwd,
|
|
446
|
+
warnReplaced: tracked === "undetermined" && !blank && current !== cwd,
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
/** The one secret-safe warning for an undetermined probe that replaced a root. */
|
|
450
|
+
export function formatUndeterminedRootReplacedWarning(relPath) {
|
|
451
|
+
return (` Warning: could not tell whether ${relPath} is tracked by git, so its BAPI_PROJECT_ROOT ` +
|
|
452
|
+
"was rewritten to this folder. Review its diff before committing.");
|
|
453
|
+
}
|
|
392
454
|
/**
|
|
393
455
|
* Resolve the non-secret identity a NEW Bridge entry should carry.
|
|
394
456
|
*
|
|
@@ -518,6 +580,20 @@ export async function runInit(cwd, options = {}) {
|
|
|
518
580
|
const targetSpec = `${MCP_PACKAGE_NAME}@${targetVersion}`;
|
|
519
581
|
const say = options.stdout ?? ((m) => console.log(m));
|
|
520
582
|
const warn = options.stderr ?? ((m) => console.error(m));
|
|
583
|
+
const trackedProbe = options.isTrackedConfig ?? defaultIsTrackedConfig(cwd);
|
|
584
|
+
const probeTracked = async (relPath) => {
|
|
585
|
+
try {
|
|
586
|
+
const verdict = await trackedProbe(relPath);
|
|
587
|
+
if (verdict === true)
|
|
588
|
+
return "tracked";
|
|
589
|
+
if (verdict === false)
|
|
590
|
+
return "untracked";
|
|
591
|
+
return verdict === "tracked" || verdict === "untracked" ? verdict : "undetermined";
|
|
592
|
+
}
|
|
593
|
+
catch {
|
|
594
|
+
return "undetermined";
|
|
595
|
+
}
|
|
596
|
+
};
|
|
521
597
|
// ---- Phase 1: IDE Detection ----
|
|
522
598
|
// BAPI-708: detection lives in `detectInitIdes` so the commit-safe asset
|
|
523
599
|
// manifest resolves the SAME conditional targets this scaffolder writes.
|
|
@@ -701,7 +777,17 @@ export async function runInit(cwd, options = {}) {
|
|
|
701
777
|
entry.args = refreshed;
|
|
702
778
|
if (!entry.env)
|
|
703
779
|
entry.env = {};
|
|
704
|
-
|
|
780
|
+
// BAPI-1318: a git-tracked config keeps its committed BAPI_PROJECT_ROOT.
|
|
781
|
+
// The probe runs only when there is a non-blank value to preserve.
|
|
782
|
+
const existingRoot = entry.env.BAPI_PROJECT_ROOT;
|
|
783
|
+
let trackedVerdict = "untracked";
|
|
784
|
+
if (typeof existingRoot === "string" && existingRoot.trim().length > 0 && existingRoot !== cwd) {
|
|
785
|
+
trackedVerdict = await probeTracked(target.path);
|
|
786
|
+
}
|
|
787
|
+
const rootDecision = decideExistingProjectRoot(existingRoot, cwd, trackedVerdict);
|
|
788
|
+
entry.env.BAPI_PROJECT_ROOT = rootDecision.value;
|
|
789
|
+
if (rootDecision.warnReplaced)
|
|
790
|
+
warn(formatUndeterminedRootReplacedWarning(target.path));
|
|
705
791
|
await io.writeFile(fullPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
|
|
706
792
|
const kind = found.action === "already-current"
|
|
707
793
|
? "already-current"
|
|
@@ -1094,9 +1180,24 @@ says to serialize structured output.
|
|
|
1094
1180
|
// the credential landed and which generated assets are safe to commit. Removed
|
|
1095
1181
|
// outright rather than behind an output-mode flag — parameterizing would change
|
|
1096
1182
|
// the injected `deps.runInit` seam signature for no user-visible gain.
|
|
1183
|
+
// BAPI-1318: a caller that already resolved the project name (install) is
|
|
1184
|
+
// authoritative, so the manifest agrees with the MCP entry. `--init` and
|
|
1185
|
+
// `upgrade` pass no creation context and keep the folder-name default.
|
|
1186
|
+
let bridgeConfigCreated = false;
|
|
1187
|
+
let bridgeConfigRepoName;
|
|
1097
1188
|
if (!bridgeConfigExists) {
|
|
1098
|
-
|
|
1099
|
-
|
|
1189
|
+
const scaffoldName = options.creationContext?.repoName ?? chooseScaffoldRepoName(cwd);
|
|
1190
|
+
const representable = validateBridgeConfigRepoNameRepresentability(scaffoldName);
|
|
1191
|
+
if (representable.ok) {
|
|
1192
|
+
await mkdir(path.dirname(bridgeConfigPath), { recursive: true });
|
|
1193
|
+
await writeFile(bridgeConfigPath, buildBridgeConfigManifest(representable.value), "utf-8");
|
|
1194
|
+
bridgeConfigCreated = true;
|
|
1195
|
+
bridgeConfigRepoName = representable.value;
|
|
1196
|
+
}
|
|
1197
|
+
else {
|
|
1198
|
+
warn(" Warning: .bridge/config was not created because this project name cannot be written " +
|
|
1199
|
+
`there (${representable.error}). Worktrees and shell tools will fall back to the git folder name.`);
|
|
1200
|
+
}
|
|
1100
1201
|
}
|
|
1101
1202
|
// BAPI-728: authoritative, secret-free outcome. `ok` is false whenever any
|
|
1102
1203
|
// entry authenticates as nobody, so no caller can report full success over a
|
|
@@ -1106,5 +1207,7 @@ says to serialize structured output.
|
|
|
1106
1207
|
configActions: structuredActions,
|
|
1107
1208
|
readinessWarnings,
|
|
1108
1209
|
targetVersion,
|
|
1210
|
+
bridgeConfigCreated,
|
|
1211
|
+
...(bridgeConfigRepoName !== undefined ? { bridgeConfigRepoName } : {}),
|
|
1109
1212
|
};
|
|
1110
1213
|
}
|
|
@@ -106,7 +106,7 @@
|
|
|
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";
|
|
@@ -167,13 +167,20 @@ export function getInstallBridgeConductorUsage() {
|
|
|
167
167
|
" --secret-name <NAME> claude-review workflow: OAuth token secret NAME",
|
|
168
168
|
" --branches a,b claude-review workflow: PR branch triggers",
|
|
169
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.",
|
|
170
175
|
" --launch-profile <name> (BAPI-1260) Opt in to the reusable epic launch: after",
|
|
171
176
|
" every mandatory prerequisite is verified, persist a",
|
|
172
177
|
" host-local profile and print the ONE drive-epic",
|
|
173
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.",
|
|
174
181
|
" --base-url <url> Explicit Bridge API target for --launch-profile.",
|
|
175
|
-
" Required there (or BAPI_BASE_URL):
|
|
176
|
-
"
|
|
182
|
+
" Required there (or BAPI_BASE_URL): guided setup",
|
|
183
|
+
" approves one named server and has no default.",
|
|
177
184
|
" --execution-mode <mode> local | durable (default: durable with",
|
|
178
185
|
" --executor-id, otherwise local).",
|
|
179
186
|
" --policy-file <path> Run policy the profile references; an unattended",
|
|
@@ -262,6 +269,7 @@ export function parseInstallBridgeConductorArgs(argv) {
|
|
|
262
269
|
let repo;
|
|
263
270
|
let dryRun = false;
|
|
264
271
|
let skipWorkflow = false;
|
|
272
|
+
let replaceDoneGate = false;
|
|
265
273
|
let reviewPolicy = DEFAULT_CONDUCTOR_REVIEW_POLICY;
|
|
266
274
|
let reviewPolicyExplicit = false;
|
|
267
275
|
let epicKey;
|
|
@@ -304,6 +312,11 @@ export function parseInstallBridgeConductorArgs(argv) {
|
|
|
304
312
|
return { status: "error", message: "--skip-workflow takes no value" };
|
|
305
313
|
skipWorkflow = true;
|
|
306
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;
|
|
307
320
|
case "--repo-wide":
|
|
308
321
|
if (inline !== undefined)
|
|
309
322
|
return { status: "error", message: "--repo-wide takes no value" };
|
|
@@ -480,6 +493,7 @@ export function parseInstallBridgeConductorArgs(argv) {
|
|
|
480
493
|
secretName,
|
|
481
494
|
branches,
|
|
482
495
|
skipWorkflow,
|
|
496
|
+
...(replaceDoneGate ? { replaceDoneGate: true } : {}),
|
|
483
497
|
executorId,
|
|
484
498
|
...(epicRunIds.length > 0 ? { epicRunIds } : {}),
|
|
485
499
|
...(repoWide ? { repoWide: true } : {}),
|
|
@@ -582,6 +596,89 @@ export async function observeExecutorReadiness(access, fetchJson, deps, options
|
|
|
582
596
|
// Runner
|
|
583
597
|
// ---------------------------------------------------------------------------
|
|
584
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
|
+
}
|
|
585
682
|
async function confirm(deps, promptText) {
|
|
586
683
|
// A missing prompt seam means no TTY. Silence is NOT consent.
|
|
587
684
|
if (!deps.promptLine)
|
|
@@ -847,6 +944,13 @@ export const CONDUCTOR_PROFILE_EMPTY_MERGE_REMEDIATION = `run \`npx ${MCP_PACKAG
|
|
|
847
944
|
/** Remediation printed whenever the managed conductor hooks were not installed. */
|
|
848
945
|
export const CONDUCTOR_HOOKS_MANUAL_REMEDIATION = "re-run `install conductor` and accept the local-observability consent, or run " +
|
|
849
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).";
|
|
850
954
|
/**
|
|
851
955
|
* Phase: tool visibility.
|
|
852
956
|
*
|
|
@@ -947,11 +1051,33 @@ async function runConductorLocalObservabilityPhase(deps, local) {
|
|
|
947
1051
|
// `reason` is already a sanitized sentence. This one IS a command failure —
|
|
948
1052
|
// a hook that cannot resolve its binary is a dead hook, and the whole point
|
|
949
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.
|
|
950
1058
|
deps.errorLog(result.error === CONDUCTOR_BIN_UNRESOLVED_ERROR
|
|
951
|
-
? ` hook installation refused — ${result.reason}`
|
|
952
|
-
:
|
|
953
|
-
|
|
954
|
-
|
|
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
|
+
}
|
|
955
1081
|
}
|
|
956
1082
|
else if (result !== null && result.ok) {
|
|
957
1083
|
if (!result.is_worktree) {
|
|
@@ -969,6 +1095,9 @@ async function runConductorLocalObservabilityPhase(deps, local) {
|
|
|
969
1095
|
? ` ${hook.name}: ${hook.action}`
|
|
970
1096
|
: ` ${hook.name}: ${hook.action} — warning: ${hook.warning}`);
|
|
971
1097
|
}
|
|
1098
|
+
if (result.partial) {
|
|
1099
|
+
deps.log(" the hook update is PARTIAL — one managed hook was published and the other was not.");
|
|
1100
|
+
}
|
|
972
1101
|
// Sanitized: the installer's own warnings are secret-free sentences, and
|
|
973
1102
|
// hook CONTENT, env, and caught errors are never rendered.
|
|
974
1103
|
for (const warning of result.warnings)
|
|
@@ -979,11 +1108,21 @@ async function runConductorLocalObservabilityPhase(deps, local) {
|
|
|
979
1108
|
// provisioned observability, and the missing reference-transaction hook
|
|
980
1109
|
// is precisely the one that stops half the events from ever being
|
|
981
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.
|
|
982
1116
|
const usable = (name) => {
|
|
983
1117
|
const hook = result.installed.find((h) => h.name === name);
|
|
984
|
-
return hook !== undefined &&
|
|
1118
|
+
return (hook !== undefined &&
|
|
1119
|
+
hook.usable === true &&
|
|
1120
|
+
hook.action !== "skipped" &&
|
|
1121
|
+
hook.warning === undefined);
|
|
985
1122
|
};
|
|
986
|
-
const complete =
|
|
1123
|
+
const complete = result.complete === true &&
|
|
1124
|
+
MANAGED_HOOK_NAMES.every((name) => usable(name)) &&
|
|
1125
|
+
result.warnings.length === 0;
|
|
987
1126
|
if (complete) {
|
|
988
1127
|
const recorded = await local.recordCapability("conductor_local_observability");
|
|
989
1128
|
if (!recorded.ok) {
|
|
@@ -1084,7 +1223,7 @@ function isHostLocalHostname(hostname) {
|
|
|
1084
1223
|
* failure there holds activation. A launcher-local listener can never pass
|
|
1085
1224
|
* here for a remote target: the server cannot reach this machine's loopback.
|
|
1086
1225
|
*/
|
|
1087
|
-
function assessNotification(policy, attended, targetBaseUrl) {
|
|
1226
|
+
function assessNotification(policy, attended, targetBaseUrl, readiness) {
|
|
1088
1227
|
if (attended) {
|
|
1089
1228
|
return { status: "not-applicable", detail: "the referenced policy is attended; no escalation webhook is required." };
|
|
1090
1229
|
}
|
|
@@ -1093,9 +1232,20 @@ function assessNotification(policy, attended, targetBaseUrl) {
|
|
|
1093
1232
|
: null;
|
|
1094
1233
|
const url = typeof notify?.webhook_url === "string" ? notify.webhook_url.trim() : "";
|
|
1095
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
|
+
}
|
|
1096
1245
|
return {
|
|
1097
1246
|
status: "missing",
|
|
1098
|
-
detail: "an unattended launch needs notify.webhook_url in its --policy-file" +
|
|
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" +
|
|
1099
1249
|
(notify && "local_sink" in notify ? " (notify.local_sink is not an escalation channel)." : "."),
|
|
1100
1250
|
};
|
|
1101
1251
|
}
|
|
@@ -1180,12 +1330,13 @@ async function runLaunchProfilePhase(deps, options, inputs) {
|
|
|
1180
1330
|
else {
|
|
1181
1331
|
deps.log(` consent: ${consent === false ? "NOT granted" : "could not be verified"}`);
|
|
1182
1332
|
gaps.push(consent === false
|
|
1183
|
-
? "unattended consent is
|
|
1184
|
-
"
|
|
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."
|
|
1185
1336
|
: "unattended consent could not be verified at the selected target (older server or unreadable report).");
|
|
1186
1337
|
}
|
|
1187
1338
|
// (4) Notification, from the server's network perspective.
|
|
1188
|
-
const notification = assessNotification(policy, attended, inputs.baseUrl);
|
|
1339
|
+
const notification = assessNotification(policy, attended, inputs.baseUrl, inputs.readiness);
|
|
1189
1340
|
deps.log(` notify: ${notification.status} — ${notification.detail}`);
|
|
1190
1341
|
if (notification.status === "missing")
|
|
1191
1342
|
gaps.push(notification.detail);
|
|
@@ -1428,16 +1579,19 @@ export async function runInstallBridgeConductorCli(argv, deps) {
|
|
|
1428
1579
|
if (options.dryRun) {
|
|
1429
1580
|
deps.log("");
|
|
1430
1581
|
deps.log("Dry run — no writes will be performed.");
|
|
1431
|
-
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:");
|
|
1432
1583
|
// Field NAMES and a COUNT only — never the check values, which are
|
|
1433
1584
|
// configuration content, and never the API key.
|
|
1434
1585
|
deps.log(" - done_gate_config.required_checks " +
|
|
1435
1586
|
`(${options.requiredChecks.length} check(s))`);
|
|
1436
|
-
deps.log(" - review_source");
|
|
1437
1587
|
if (options.reviewPolicyExplicit)
|
|
1438
1588
|
deps.log(" - review_policy");
|
|
1589
|
+
if (options.replaceDoneGate)
|
|
1590
|
+
deps.log(" - replace_done_gate");
|
|
1439
1591
|
deps.log(" - auto_merge_enabled");
|
|
1440
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.");
|
|
1441
1595
|
deps.log(` Would target base URL: ${baseUrl}`);
|
|
1442
1596
|
deps.log(options.skipWorkflow || options.reviewPolicy !== "verdict_protocol"
|
|
1443
1597
|
? " Workflow scaffold: skipped (not applicable for this policy)"
|
|
@@ -1480,6 +1634,40 @@ export async function runInstallBridgeConductorCli(argv, deps) {
|
|
|
1480
1634
|
deps.log("complete-with-manual-steps (dry run)");
|
|
1481
1635
|
return 0;
|
|
1482
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);
|
|
1483
1671
|
// ---- Consent #2: the bootstrap write itself ----------------------------
|
|
1484
1672
|
// Deliberately separate from the base-URL consent above (which was given
|
|
1485
1673
|
// before any network call). Confirming WHERE to write is not consent TO write.
|
|
@@ -1489,36 +1677,36 @@ export async function runInstallBridgeConductorCli(argv, deps) {
|
|
|
1489
1677
|
deps.log("Aborted — no server write was attempted.");
|
|
1490
1678
|
return 0;
|
|
1491
1679
|
}
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
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
|
+
};
|
|
1497
1696
|
}
|
|
1498
|
-
// ---- Phase 2: bootstrap ------------------------------------------------
|
|
1499
|
-
deps.log("");
|
|
1500
|
-
deps.log("Phase 2/8 — bootstrapping supervisor project defaults…");
|
|
1501
1697
|
let bootstrapResponse;
|
|
1502
1698
|
try {
|
|
1503
|
-
bootstrapResponse = await bootstrapConductorSupervisorDefaults(access,
|
|
1504
|
-
required_checks: options.requiredChecks,
|
|
1505
|
-
review_source: "verdict_protocol",
|
|
1506
|
-
// BAPI-694 — the narrow ReviewPolicy object, sent ONLY on an explicit
|
|
1507
|
-
// selection. `review_source` above is deliberately left alone: it is the
|
|
1508
|
-
// WIDER done-gate vocabulary and the two are not interchangeable, so
|
|
1509
|
-
// neither value is coerced into the other.
|
|
1510
|
-
...(options.reviewPolicyExplicit
|
|
1511
|
-
? { review_policy: { source: options.reviewPolicy } }
|
|
1512
|
-
: {}),
|
|
1513
|
-
merge_approval_required: "true",
|
|
1514
|
-
}, deps.postJson);
|
|
1699
|
+
bootstrapResponse = await bootstrapConductorSupervisorDefaults(access, applyRequest, deps.postJson);
|
|
1515
1700
|
}
|
|
1516
1701
|
catch (err) {
|
|
1517
|
-
|
|
1518
|
-
|
|
1702
|
+
reportBootstrapFailure(deps, "apply", classifySupervisorBootstrapFailure(err));
|
|
1703
|
+
return 1;
|
|
1704
|
+
}
|
|
1705
|
+
if (bootstrapResponse.setup_outcome === null || bootstrapResponse.config_outcome === null) {
|
|
1706
|
+
reportBootstrapFailure(deps, "apply", "unsupported-protocol");
|
|
1519
1707
|
return 1;
|
|
1520
1708
|
}
|
|
1521
|
-
deps
|
|
1709
|
+
renderBootstrapOutcome(deps, bootstrapResponse);
|
|
1522
1710
|
// ---- Phase 3: optional workflow scaffold -------------------------------
|
|
1523
1711
|
deps.log("");
|
|
1524
1712
|
deps.log("Phase 3/8 — claude-review workflow…");
|
|
@@ -1609,8 +1797,10 @@ export async function runInstallBridgeConductorCli(argv, deps) {
|
|
|
1609
1797
|
manualActions.push("local observability: run the packaged `install conductor` command, or " +
|
|
1610
1798
|
"`epic-implementer install-git-hooks`, to install the managed conductor Git hooks.");
|
|
1611
1799
|
}
|
|
1612
|
-
// The fail-loud hook-binary refusal
|
|
1613
|
-
// 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.
|
|
1614
1804
|
if (observabilityFatal)
|
|
1615
1805
|
return 1;
|
|
1616
1806
|
// ---- Phase 6: setup-epic verification (conditional) --------------------
|