@bridge_gpt/mcp-server 0.2.59 → 0.2.60
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 +173 -51
- package/build/agent-guidance.generated.js +1 -1
- package/build/agent-notes.js +7 -1
- package/build/commands.generated.js +4 -4
- package/build/conductor/bridge-api-client.js +53 -2
- package/build/conductor/doctor.js +255 -53
- package/build/conductor/epic-runtime.js +11 -2
- package/build/conductor/install-doctor.js +24 -2
- package/build/conductor/local-merge.js +319 -26
- package/build/conductor/merge-execution-contract.js +61 -0
- package/build/conductor/merge-execution-eligibility.js +129 -0
- package/build/conductor/readiness-sections.js +159 -2
- package/build/conductor/readiness.js +95 -14
- package/build/conductor/recovery-operations.js +8 -2
- package/build/connect-bitbucket-api.js +6 -1
- package/build/connect-bitbucket.js +398 -88
- package/build/credential-store.js +16 -0
- package/build/docs.generated.js +2 -2
- package/build/doctor.js +183 -9
- package/build/drive-epic.js +23 -0
- package/build/epic-implementer-bin.js +10 -10
- package/build/epic-implementer-claude-hook-bin.js +1 -1
- package/build/epic-implementer-env.js +43 -14
- package/build/executor/ci-rerun-job.js +269 -0
- package/build/executor/cli.js +31 -0
- package/build/executor/credentials.js +11 -0
- package/build/executor/env.js +60 -0
- package/build/executor/index.js +21 -4
- package/build/executor/job-runner.js +126 -0
- package/build/executor/job-types.js +12 -2
- package/build/executor/merge-job.js +11 -2
- package/build/executor/service-lifecycle.js +136 -0
- package/build/executor/service-unit.js +427 -9
- package/build/executor/types.js +5 -0
- package/build/index.js +194 -179
- package/build/install-bridge-conductor.js +201 -8
- package/build/install-bridge.js +356 -30
- package/build/mcp-install-state.js +42 -4
- package/build/merge-pull-request.js +103 -10
- package/build/merge-workflow-codeowners.js +224 -0
- package/build/merge-workflow-config.js +133 -0
- package/build/merge-workflow-drift.js +66 -0
- package/build/merge-workflow-install.js +638 -0
- package/build/merge-workflow.js +572 -0
- package/build/pipelines.generated.js +2 -2
- package/build/plane/cli.js +24 -2
- package/build/plane/preflight.js +68 -0
- package/build/readme.generated.js +1 -1
- package/build/run-unit-tests-launcher.js +5 -0
- package/build/setup-epic.js +163 -9
- package/build/version.generated.js +3 -3
- package/package.json +3 -3
- package/build/conduct-epic/bridge-client.js +0 -773
- package/build/conduct-epic/checkpoint-store.js +0 -542
- package/build/conduct-epic/cli.js +0 -3145
- package/build/conduct-epic/cut-protocol.js +0 -392
- package/build/conduct-epic/lock.js +0 -302
- package/build/conduct-epic/pr-state.js +0 -286
- package/build/conduct-epic/spawn.js +0 -113
- package/build/conductor/cli.js +0 -1278
- package/build/conductor-bundle-cli.js +0 -256
- package/build/direct-ticket-tools.js +0 -70
- package/build/executor/reconcile-overlap-governance.js +0 -129
- package/build/implement-epic/bridge-client.js +0 -773
- package/build/implement-epic/checkpoint-store.js +0 -542
- package/build/implement-epic/cli.js +0 -3158
- package/build/implement-epic/cut-protocol.js +0 -392
- package/build/implement-epic/lock.js +0 -302
- package/build/implement-epic/pr-state.js +0 -286
- package/build/implement-epic/spawn.js +0 -113
- package/build/plan-epic-conductor-eligibility.js +0 -213
|
@@ -364,8 +364,15 @@ export function safeDiagnosticMessage(err, fallback) {
|
|
|
364
364
|
}
|
|
365
365
|
return fallback;
|
|
366
366
|
}
|
|
367
|
-
/**
|
|
368
|
-
|
|
367
|
+
/**
|
|
368
|
+
* GET auth headers. The API key travels ONLY in a header, never in the URL.
|
|
369
|
+
*
|
|
370
|
+
* Exported (BAPI-1133) so a caller outside this module — `setup-epic.ts`'s
|
|
371
|
+
* {@link ConductorBridgeApiAccess}-based adapter for
|
|
372
|
+
* `resolveMergeExecutionMode`'s narrow dependency shape — can build the same
|
|
373
|
+
* headers without a second copy of this one-line mapping.
|
|
374
|
+
*/
|
|
375
|
+
export function conductorGetHeaders(access) {
|
|
369
376
|
return { "X-API-Key": access.apiKey };
|
|
370
377
|
}
|
|
371
378
|
/**
|
|
@@ -2089,6 +2096,8 @@ export const CONDUCTOR_REVIEW_ALIGNMENT_STATUSES = new Set([
|
|
|
2089
2096
|
export const READINESS_FRESH_RECONCILER_COUNT_CAP = 2;
|
|
2090
2097
|
const READINESS_SOURCES = new Set(["epic", "project_default", "none"]);
|
|
2091
2098
|
const ACTIONS_LEVELS = new Set(["write", "read", "none", "unknown"]);
|
|
2099
|
+
/** The one scope the server reports for the merge-execution block (BAPI-1135). */
|
|
2100
|
+
const MERGE_EXECUTION_BRANCH_SCOPES = new Set(["default_branch"]);
|
|
2092
2101
|
function readinessShapeError() {
|
|
2093
2102
|
// Deliberately carries NO field name, value, or payload excerpt: a malformed
|
|
2094
2103
|
// body is untrusted input and its contents must not reach output.
|
|
@@ -2240,6 +2249,48 @@ export function parseConductorReadinessResponse(body) {
|
|
|
2240
2249
|
conductor_ci_workflow: parseConductorCiWorkflow(root),
|
|
2241
2250
|
unattended: parseUnattended(root),
|
|
2242
2251
|
bring_up: parseBringUp(root),
|
|
2252
|
+
merge_execution: parseMergeExecution(root),
|
|
2253
|
+
};
|
|
2254
|
+
}
|
|
2255
|
+
/**
|
|
2256
|
+
* Parse the optional `merge_execution` block (BAPI-1135).
|
|
2257
|
+
*
|
|
2258
|
+
* Same older-server rule as every optional block above: absent (or null) is
|
|
2259
|
+
* `null`, and a PRESENT block is validated strictly — every boolean through
|
|
2260
|
+
* `requireBool`, the scope through the shared enum parser, and the version as
|
|
2261
|
+
* either `null` or a non-negative integer through `requireInt`.
|
|
2262
|
+
*
|
|
2263
|
+
* A malformed PRESENT block or a malformed present FIELD throws, and is never
|
|
2264
|
+
* degraded to `null`. The distinction is load-bearing: `null` means "this server
|
|
2265
|
+
* does not report it", which is a server-version fact, while a block that claims
|
|
2266
|
+
* to carry merge eligibility and cannot be read is untrusted input. Silently
|
|
2267
|
+
* turning the second into the first would let a corrupt body masquerade as an
|
|
2268
|
+
* older deploy, and the two have different remediations.
|
|
2269
|
+
*/
|
|
2270
|
+
function parseMergeExecution(body) {
|
|
2271
|
+
const raw = body.merge_execution;
|
|
2272
|
+
if (raw === undefined || raw === null)
|
|
2273
|
+
return null;
|
|
2274
|
+
const o = requireObject(raw);
|
|
2275
|
+
const version = o.merge_workflow_version;
|
|
2276
|
+
let parsedVersion;
|
|
2277
|
+
if (version === null || version === undefined) {
|
|
2278
|
+
parsedVersion = null;
|
|
2279
|
+
}
|
|
2280
|
+
else {
|
|
2281
|
+
parsedVersion = requireInt(o, "merge_workflow_version");
|
|
2282
|
+
if (parsedVersion < 0)
|
|
2283
|
+
throw readinessShapeError();
|
|
2284
|
+
}
|
|
2285
|
+
return {
|
|
2286
|
+
branch_scope: requireEnum(o, "branch_scope", MERGE_EXECUTION_BRANCH_SCOPES),
|
|
2287
|
+
branch_protection_probe_succeeded: requireBool(o, "branch_protection_probe_succeeded"),
|
|
2288
|
+
branch_protection_actions_bot_admitted: requireBool(o, "branch_protection_actions_bot_admitted"),
|
|
2289
|
+
merge_workflow_probe_succeeded: requireBool(o, "merge_workflow_probe_succeeded"),
|
|
2290
|
+
merge_workflow_present: requireBool(o, "merge_workflow_present"),
|
|
2291
|
+
merge_workflow_enabled: requireBool(o, "merge_workflow_enabled"),
|
|
2292
|
+
merge_workflow_version: parsedVersion,
|
|
2293
|
+
merge_workflow_token_write_authority: requireBool(o, "merge_workflow_token_write_authority"),
|
|
2243
2294
|
};
|
|
2244
2295
|
}
|
|
2245
2296
|
/**
|
|
@@ -13,6 +13,7 @@ import os from "node:os";
|
|
|
13
13
|
import { doctorConductorLedger } from "./store.js";
|
|
14
14
|
import { detectClaudeLogin, formatClaudeLoginAdvisory } from "../claude-login.js";
|
|
15
15
|
import { inspectConductorGitHooks } from "./git-hooks.js";
|
|
16
|
+
import { MERGE_WORKFLOW_IDENTIFIER } from "./merge-execution-contract.js";
|
|
16
17
|
import { resolveMcpShimInvocationForRuntime, resolvePackageRootFromModuleUrl, } from "../mcp-server-invocation.js";
|
|
17
18
|
import { MCP_PACKAGE_NAME } from "../mcp-identity.js";
|
|
18
19
|
import { resolveProfiles } from "../mcp-profile.js";
|
|
@@ -38,13 +39,24 @@ export const MCP_PROFILE_REMEDIATION = "ADD `epic_implementer` to BRIDGE_MCP_PRO
|
|
|
38
39
|
* is blocked on epic A2 (App elevation to `contents: write`); until A2 ships,
|
|
39
40
|
* `gh` is a real prerequisite and this stays a named check.
|
|
40
41
|
*/
|
|
41
|
-
export const GH_CLI_REMEDIATION = "install the GitHub CLI (`gh`) and put it on PATH; conductor-driven merges
|
|
42
|
-
"
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
42
|
+
export const GH_CLI_REMEDIATION = "install the GitHub CLI (`gh`) and put it on PATH; conductor-driven merges shell out to it " +
|
|
43
|
+
"because the GitHub App token is `contents: read` and cannot merge. Since BAPI-1136 `gh` " +
|
|
44
|
+
"authenticates with the configured merge credential, not with this host's own `gh` login.";
|
|
45
|
+
/**
|
|
46
|
+
* Remediation for a host with no configured merge credential (BAPI-1136).
|
|
47
|
+
*
|
|
48
|
+
* This REPLACED a `gh auth login` remediation, and the replacement is the whole
|
|
49
|
+
* point. After the merge stopped authenticating as whoever is logged into `gh`,
|
|
50
|
+
* that instruction sent an operator to fix something the merge no longer reads —
|
|
51
|
+
* the most expensive kind of wrong hint, because following it looks reasonable
|
|
52
|
+
* and changes nothing. `merge-pull-request.ts` says exactly that about the same
|
|
53
|
+
* text; this is the last place it was still live.
|
|
54
|
+
*/
|
|
55
|
+
export const GH_AUTH_REMEDIATION = "set BAPI_LOCAL_MERGE_TOKEN (or BAPI_MACHINE_MERGE_TOKEN for machine-user mode) in the " +
|
|
56
|
+
"environment this host launches the conductor with; local merge has no ambient-login " +
|
|
57
|
+
"fallback and fails closed with a named reason when neither is configured.";
|
|
58
|
+
/** Detail used when the credential check was not reached because `gh` is absent. */
|
|
59
|
+
export const GH_AUTH_SKIPPED_DETAIL = "not tested — `gh` is not installed, so no merge could run regardless of credential";
|
|
48
60
|
/**
|
|
49
61
|
* Remediation for a native ledger whose binding does not load.
|
|
50
62
|
*
|
|
@@ -422,7 +434,7 @@ export function inspectMcpProfile(env) {
|
|
|
422
434
|
* `gh auth status` (no writes, no merge) and reports whether the host could merge
|
|
423
435
|
* a PR locally if `policy_json.local_merge.enabled` were set. Never throws.
|
|
424
436
|
*/
|
|
425
|
-
export function inspectLocalMerge(runCommand) {
|
|
437
|
+
export function inspectLocalMerge(runCommand, env = process.env) {
|
|
426
438
|
const run = runCommand ??
|
|
427
439
|
((cmd, args) => {
|
|
428
440
|
try {
|
|
@@ -447,48 +459,51 @@ export function inspectLocalMerge(runCommand) {
|
|
|
447
459
|
}
|
|
448
460
|
});
|
|
449
461
|
let gh_available = false;
|
|
450
|
-
let gh_authed = false;
|
|
451
462
|
try {
|
|
452
463
|
gh_available = run("gh", ["--version"]).status === 0;
|
|
453
464
|
}
|
|
454
465
|
catch {
|
|
455
466
|
gh_available = false;
|
|
456
467
|
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
468
|
+
// BAPI-1136. This used to run `gh auth status` and call the result
|
|
469
|
+
// `gh_authed`. That probe measured the ambient session, which the merge stopped
|
|
470
|
+
// consulting — so it reported PASS on a host with a logged-in `gh` and no
|
|
471
|
+
// configured credential (every merge then failing
|
|
472
|
+
// `local_merge_credential_missing`), and WARN on a host with the credential set
|
|
473
|
+
// and no ambient login (where merges work). Both directions wrong.
|
|
474
|
+
//
|
|
475
|
+
// The replacement asks the question the merge actually asks, and answers it
|
|
476
|
+
// read-only from the environment — no subprocess, no network, and no credential
|
|
477
|
+
// VALUE anywhere in the result. Either mode's key satisfies it: which one a run
|
|
478
|
+
// uses is the server-resolved typed mode, which a local doctor cannot know.
|
|
479
|
+
const merge_credential_configured = ["BAPI_LOCAL_MERGE_TOKEN", "BAPI_MACHINE_MERGE_TOKEN"].some((key) => typeof env[key] === "string" && env[key].trim().length > 0);
|
|
465
480
|
const warnings = [];
|
|
466
481
|
if (!gh_available) {
|
|
467
482
|
warnings.push("`gh` is not installed or not on PATH. Local merge (policy_json.local_merge.enabled) " +
|
|
468
483
|
"cannot run; install the GitHub CLI to enable conductor-driven merges.");
|
|
469
484
|
}
|
|
470
|
-
else if (!
|
|
471
|
-
warnings.push("`gh` is installed but
|
|
472
|
-
"
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
//
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
//
|
|
479
|
-
//
|
|
480
|
-
//
|
|
481
|
-
//
|
|
482
|
-
//
|
|
485
|
+
else if (!merge_credential_configured) {
|
|
486
|
+
warnings.push("`gh` is installed but no merge credential is configured. Set BAPI_LOCAL_MERGE_TOKEN " +
|
|
487
|
+
"(or BAPI_MACHINE_MERGE_TOKEN for machine-user mode); local merge has no ambient-login " +
|
|
488
|
+
"fallback and fails closed with a named reason.");
|
|
489
|
+
}
|
|
490
|
+
// Capability gap only — local merge is opt-in, so an unconfigured host is not a
|
|
491
|
+
// hard failure unless the operator enabled it. Reported as degraded for visibility.
|
|
492
|
+
const degraded = !gh_available || !merge_credential_configured;
|
|
493
|
+
// TWO outcomes, not one (BAPI-1055): installing `gh` and configuring the merge
|
|
494
|
+
// credential are independently actionable, and `degraded` stays the same
|
|
495
|
+
// `!gh_available || !<second check>` conjunction it has always been. When `gh`
|
|
496
|
+
// is absent the second outcome is `skip`, never `fail` — nothing could merge on
|
|
497
|
+
// that host regardless of credential, so reporting a credential failure would
|
|
498
|
+
// name the wrong fix.
|
|
483
499
|
//
|
|
484
|
-
// AC-7
|
|
485
|
-
//
|
|
486
|
-
//
|
|
487
|
-
//
|
|
488
|
-
//
|
|
489
|
-
//
|
|
490
|
-
//
|
|
491
|
-
// altered by reporting them.
|
|
500
|
+
// AC-7 RETIRED HERE (BAPI-1136). The note this replaces said these checks were
|
|
501
|
+
// load-bearing because "conductor merges run under this machine's own `gh`
|
|
502
|
+
// auth", and named epic A2 as the owner of their retirement. This IS that A2
|
|
503
|
+
// child: the merge now authenticates with one explicitly configured,
|
|
504
|
+
// customer-held credential and never with the ambient session. So the check is
|
|
505
|
+
// not removed — it is re-pointed at the thing that now decides, which is the
|
|
506
|
+
// only way it stays load-bearing rather than becoming decorative.
|
|
492
507
|
const outcomes = [
|
|
493
508
|
gh_available
|
|
494
509
|
? {
|
|
@@ -506,28 +521,155 @@ export function inspectLocalMerge(runCommand) {
|
|
|
506
521
|
},
|
|
507
522
|
!gh_available
|
|
508
523
|
? {
|
|
509
|
-
id: "
|
|
510
|
-
label: "
|
|
524
|
+
id: "merge-credential",
|
|
525
|
+
label: "Configured merge credential",
|
|
511
526
|
status: "skip",
|
|
512
527
|
detail: GH_AUTH_SKIPPED_DETAIL,
|
|
513
528
|
remediation: GH_CLI_REMEDIATION,
|
|
514
529
|
}
|
|
515
|
-
:
|
|
530
|
+
: merge_credential_configured
|
|
516
531
|
? {
|
|
517
|
-
id: "
|
|
518
|
-
label: "
|
|
532
|
+
id: "merge-credential",
|
|
533
|
+
label: "Configured merge credential",
|
|
519
534
|
status: "pass",
|
|
520
|
-
detail: "
|
|
535
|
+
detail: "BAPI_LOCAL_MERGE_TOKEN or BAPI_MACHINE_MERGE_TOKEN is set",
|
|
521
536
|
}
|
|
522
537
|
: {
|
|
523
|
-
id: "
|
|
524
|
-
label: "
|
|
538
|
+
id: "merge-credential",
|
|
539
|
+
label: "Configured merge credential",
|
|
525
540
|
status: "warn",
|
|
526
|
-
detail: "`gh` is installed but
|
|
541
|
+
detail: "`gh` is installed but neither BAPI_LOCAL_MERGE_TOKEN nor " +
|
|
542
|
+
"BAPI_MACHINE_MERGE_TOKEN is set, so a local merge would fail closed",
|
|
527
543
|
remediation: GH_AUTH_REMEDIATION,
|
|
528
544
|
},
|
|
529
545
|
];
|
|
530
|
-
return { gh_available,
|
|
546
|
+
return { gh_available, merge_credential_configured, degraded, warnings, outcomes };
|
|
547
|
+
}
|
|
548
|
+
/** Remediation when this host has no `gh` identity to observe with. */
|
|
549
|
+
export const GH_ACTIONS_MERGE_AUTH_REMEDIATION = "run `gh auth login` so this host has a GitHub identity; without one it cannot " +
|
|
550
|
+
"observe the merge capability. This does not affect whether the capability exists.";
|
|
551
|
+
/** Remediation when the capability could not be observed from this host. */
|
|
552
|
+
export const GH_ACTIONS_MERGE_UNOBSERVABLE_REMEDIATION = "check network connectivity to api.github.com and `gh auth status`; the merge " +
|
|
553
|
+
"capability could NOT BE OBSERVED from this host, which is not the same as it " +
|
|
554
|
+
"being absent. The server's conductor readiness report is authoritative.";
|
|
555
|
+
/** Detail for the observability outcome when the auth prerequisite never held. */
|
|
556
|
+
export const GH_ACTIONS_MERGE_SKIPPED_DETAIL = "not attempted — this host has no authenticated `gh` identity";
|
|
557
|
+
/**
|
|
558
|
+
* Read-only probe of whether this host can observe the `github_actions` merge
|
|
559
|
+
* capability. Never throws.
|
|
560
|
+
*
|
|
561
|
+
* Two bounded, read-only commands and nothing else: `gh auth status`, then a
|
|
562
|
+
* status-only `gh api` GET of the canonical workflow metadata endpoint. The
|
|
563
|
+
* injected runner returns a STATUS ONLY — it is deliberately not widened to
|
|
564
|
+
* capture stdout, because parsing that output would mean re-deriving workflow
|
|
565
|
+
* state the server already reports, from a different identity, with no way to
|
|
566
|
+
* reconcile a disagreement.
|
|
567
|
+
*
|
|
568
|
+
* Every failure mode — a timeout, a non-zero status, a missing `gh`, a thrown
|
|
569
|
+
* dependency error — degrades to `false` with a retry-oriented warning. None
|
|
570
|
+
* raises a backend platform alert: an unobservable capability is a LOCAL
|
|
571
|
+
* condition, and paging on it would page for every laptop behind a proxy.
|
|
572
|
+
*
|
|
573
|
+
* Emits only status classifications and fixed remediation text. No process
|
|
574
|
+
* output, auth detail, repository URL, or command credential appears anywhere in
|
|
575
|
+
* the result.
|
|
576
|
+
*/
|
|
577
|
+
export function inspectGitHubActionsMerge(runCommand) {
|
|
578
|
+
const run = runCommand ??
|
|
579
|
+
((cmd, args) => {
|
|
580
|
+
try {
|
|
581
|
+
// Same structure and bound as `inspectLocalMerge`: a static ESM import,
|
|
582
|
+
// a 10s timeout so a network stall cannot hold the doctor past it, and
|
|
583
|
+
// GH_PROMPT_DISABLED so an interactive prompt cannot either.
|
|
584
|
+
const r = spawnSync(cmd, args, {
|
|
585
|
+
encoding: "utf8",
|
|
586
|
+
timeout: 10_000,
|
|
587
|
+
env: { ...process.env, GH_PROMPT_DISABLED: "1" },
|
|
588
|
+
});
|
|
589
|
+
return { status: r.status };
|
|
590
|
+
}
|
|
591
|
+
catch {
|
|
592
|
+
return { status: null };
|
|
593
|
+
}
|
|
594
|
+
});
|
|
595
|
+
let gh_authed = false;
|
|
596
|
+
try {
|
|
597
|
+
gh_authed = run("gh", ["auth", "status"]).status === 0;
|
|
598
|
+
}
|
|
599
|
+
catch {
|
|
600
|
+
gh_authed = false;
|
|
601
|
+
}
|
|
602
|
+
let capability_observable = false;
|
|
603
|
+
if (gh_authed) {
|
|
604
|
+
try {
|
|
605
|
+
// `--silent` keeps the body out of the child's stdout entirely: this probe
|
|
606
|
+
// wants the STATUS, and a body it never requests is a body it cannot leak.
|
|
607
|
+
capability_observable =
|
|
608
|
+
run("gh", [
|
|
609
|
+
"api",
|
|
610
|
+
"--silent",
|
|
611
|
+
`repos/{owner}/{repo}/actions/workflows/${MERGE_WORKFLOW_IDENTIFIER}`,
|
|
612
|
+
]).status === 0;
|
|
613
|
+
}
|
|
614
|
+
catch {
|
|
615
|
+
capability_observable = false;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
const warnings = [];
|
|
619
|
+
if (!gh_authed) {
|
|
620
|
+
warnings.push(GH_ACTIONS_MERGE_AUTH_REMEDIATION);
|
|
621
|
+
}
|
|
622
|
+
else if (!capability_observable) {
|
|
623
|
+
warnings.push(GH_ACTIONS_MERGE_UNOBSERVABLE_REMEDIATION);
|
|
624
|
+
}
|
|
625
|
+
const outcomes = [
|
|
626
|
+
gh_authed
|
|
627
|
+
? {
|
|
628
|
+
id: "gh-actions-merge-auth",
|
|
629
|
+
label: "GitHub Actions merge: local observation prerequisites",
|
|
630
|
+
status: "pass",
|
|
631
|
+
detail: "eligible to observe — `gh auth status` reports an authenticated account",
|
|
632
|
+
}
|
|
633
|
+
: {
|
|
634
|
+
id: "gh-actions-merge-auth",
|
|
635
|
+
label: "GitHub Actions merge: local observation prerequisites",
|
|
636
|
+
status: "warn",
|
|
637
|
+
detail: "not observable — no authenticated `gh` identity on this host",
|
|
638
|
+
remediation: GH_ACTIONS_MERGE_AUTH_REMEDIATION,
|
|
639
|
+
},
|
|
640
|
+
!gh_authed
|
|
641
|
+
? {
|
|
642
|
+
// `skip`, never `fail`: observability was never tested, and claiming it
|
|
643
|
+
// failed would send the operator to debug connectivity on a host that
|
|
644
|
+
// simply has no identity yet.
|
|
645
|
+
id: "gh-actions-merge-observable",
|
|
646
|
+
label: "GitHub Actions merge: capability observability",
|
|
647
|
+
status: "skip",
|
|
648
|
+
detail: GH_ACTIONS_MERGE_SKIPPED_DETAIL,
|
|
649
|
+
remediation: GH_ACTIONS_MERGE_AUTH_REMEDIATION,
|
|
650
|
+
}
|
|
651
|
+
: capability_observable
|
|
652
|
+
? {
|
|
653
|
+
id: "gh-actions-merge-observable",
|
|
654
|
+
label: "GitHub Actions merge: capability observability",
|
|
655
|
+
status: "pass",
|
|
656
|
+
detail: "eligible to observe — the workflow metadata endpoint answered",
|
|
657
|
+
}
|
|
658
|
+
: {
|
|
659
|
+
id: "gh-actions-merge-observable",
|
|
660
|
+
label: "GitHub Actions merge: capability observability",
|
|
661
|
+
status: "warn",
|
|
662
|
+
detail: "could not verify — the workflow metadata endpoint could not be read",
|
|
663
|
+
remediation: GH_ACTIONS_MERGE_UNOBSERVABLE_REMEDIATION,
|
|
664
|
+
},
|
|
665
|
+
];
|
|
666
|
+
return {
|
|
667
|
+
gh_authed,
|
|
668
|
+
capability_observable,
|
|
669
|
+
degraded: !gh_authed || !capability_observable,
|
|
670
|
+
warnings,
|
|
671
|
+
outcomes,
|
|
672
|
+
};
|
|
531
673
|
}
|
|
532
674
|
/**
|
|
533
675
|
* Build the combined read-only doctor report. Composes the existing ledger
|
|
@@ -553,12 +695,43 @@ export async function buildConductorDoctorReport(deps = {}) {
|
|
|
553
695
|
git_hooks: inspectHooks(deps.hooksDeps),
|
|
554
696
|
mcp_profile,
|
|
555
697
|
local_merge: inspectLocalMerge(deps.runCommand),
|
|
698
|
+
github_actions_merge: inspectGitHubActionsMergeSafe(deps),
|
|
556
699
|
native_ledger,
|
|
557
700
|
deny_enforcement,
|
|
558
701
|
claude_login,
|
|
559
702
|
repair_credentials: await inspectRepairVerbCredentials(deps.env ?? process.env, deps.resolveRepairAccess),
|
|
560
703
|
};
|
|
561
704
|
}
|
|
705
|
+
/**
|
|
706
|
+
* Run the GitHub Actions merge inspector, containing any throw as degraded data.
|
|
707
|
+
*
|
|
708
|
+
* The production inspector is already exception-safe; this is defense in depth
|
|
709
|
+
* for an INJECTED fake, so a test double that throws degrades the section rather
|
|
710
|
+
* than failing the whole doctor report.
|
|
711
|
+
*/
|
|
712
|
+
function inspectGitHubActionsMergeSafe(deps) {
|
|
713
|
+
const inspect = deps.inspectGitHubActionsMerge ?? inspectGitHubActionsMerge;
|
|
714
|
+
try {
|
|
715
|
+
return inspect(deps.runCommand);
|
|
716
|
+
}
|
|
717
|
+
catch {
|
|
718
|
+
return {
|
|
719
|
+
gh_authed: false,
|
|
720
|
+
capability_observable: false,
|
|
721
|
+
degraded: true,
|
|
722
|
+
warnings: [GH_ACTIONS_MERGE_UNOBSERVABLE_REMEDIATION],
|
|
723
|
+
outcomes: [
|
|
724
|
+
{
|
|
725
|
+
id: "gh-actions-merge-observable",
|
|
726
|
+
label: "GitHub Actions merge: capability observability",
|
|
727
|
+
status: "warn",
|
|
728
|
+
detail: "could not verify — the local inspection could not be completed",
|
|
729
|
+
remediation: GH_ACTIONS_MERGE_UNOBSERVABLE_REMEDIATION,
|
|
730
|
+
},
|
|
731
|
+
],
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
}
|
|
562
735
|
/**
|
|
563
736
|
* Run the deny-enforcement inspector and convert any thrown exception into degraded
|
|
564
737
|
* report data — a deny-probe failure must never throw out of doctor. The underlying
|
|
@@ -598,7 +771,7 @@ async function inspectDenyEnforcementSafe(inspect) {
|
|
|
598
771
|
* hierarchy across sections.
|
|
599
772
|
*/
|
|
600
773
|
export function formatConductorDoctorReport(report) {
|
|
601
|
-
const { ledger, git_hooks, mcp_profile, local_merge, native_ledger, deny_enforcement, claude_login } = report;
|
|
774
|
+
const { ledger, git_hooks, mcp_profile, local_merge, github_actions_merge, native_ledger, deny_enforcement, claude_login } = report;
|
|
602
775
|
const lines = [
|
|
603
776
|
"Conductor ledger doctor",
|
|
604
777
|
"───────────────────────",
|
|
@@ -666,13 +839,38 @@ export function formatConductorDoctorReport(report) {
|
|
|
666
839
|
lines.push("Local merge capability (optional, opt-in)");
|
|
667
840
|
lines.push("─────────────────────────────────────────");
|
|
668
841
|
const ghTag = local_merge.gh_available
|
|
669
|
-
? local_merge.
|
|
842
|
+
? local_merge.merge_credential_configured
|
|
670
843
|
? "[OK]"
|
|
671
|
-
: "[WARNING]
|
|
844
|
+
: "[WARNING] no merge credential configured"
|
|
672
845
|
: "[WARNING] not installed";
|
|
673
846
|
lines.push(`gh available: ${local_merge.gh_available} ${ghTag}`);
|
|
674
|
-
lines.push(`
|
|
847
|
+
lines.push(`merge credential set: ${local_merge.merge_credential_configured}`);
|
|
675
848
|
lines.push(`degraded: ${local_merge.degraded}`);
|
|
849
|
+
// BAPI-1135 — the two observations render INDEPENDENTLY, each leading with its
|
|
850
|
+
// own decisive word, because they have two different fixes. Collapsing them
|
|
851
|
+
// into one line would hide which of the two an operator has to act on.
|
|
852
|
+
//
|
|
853
|
+
// The vocabulary is deliberate: `not observable` is a statement about THIS
|
|
854
|
+
// HOST, and `could not verify` is a statement about a read that failed.
|
|
855
|
+
// Neither ever says the capability is absent — only the server's readiness
|
|
856
|
+
// report may say that.
|
|
857
|
+
if (github_actions_merge) {
|
|
858
|
+
lines.push("");
|
|
859
|
+
lines.push("GitHub Actions merge (read-only observability)");
|
|
860
|
+
lines.push(`gh authenticated: ${github_actions_merge.gh_authed} ${github_actions_merge.gh_authed ? "(eligible to observe)" : "(not observable)"}`);
|
|
861
|
+
lines.push(`capability observable: ${github_actions_merge.capability_observable} ${github_actions_merge.gh_authed
|
|
862
|
+
? github_actions_merge.capability_observable
|
|
863
|
+
? "(eligible to observe)"
|
|
864
|
+
: "(could not verify)"
|
|
865
|
+
: "(not attempted)"}`);
|
|
866
|
+
lines.push(`degraded: ${github_actions_merge.degraded}`);
|
|
867
|
+
if (github_actions_merge.warnings.length > 0) {
|
|
868
|
+
lines.push("warnings:");
|
|
869
|
+
for (const w of github_actions_merge.warnings)
|
|
870
|
+
lines.push(` - ${w}`);
|
|
871
|
+
}
|
|
872
|
+
lines.push("");
|
|
873
|
+
}
|
|
676
874
|
if (local_merge.warnings.length > 0) {
|
|
677
875
|
lines.push("local merge warnings:");
|
|
678
876
|
for (const w of local_merge.warnings)
|
|
@@ -753,7 +951,7 @@ export const CONDUCTOR_READINESS_DESCRIPTORS = [
|
|
|
753
951
|
{ id: "git-hooks", label: "Conductor git hooks" },
|
|
754
952
|
{ id: "mcp-profile", label: "Epic Implementer MCP profile" },
|
|
755
953
|
{ id: "gh-cli", label: "GitHub CLI availability" },
|
|
756
|
-
{ id: "
|
|
954
|
+
{ id: "merge-credential", label: "Configured merge credential" },
|
|
757
955
|
{ id: "native-ledger", label: "Native ledger binding" },
|
|
758
956
|
{ id: "mcp-registration-form", label: "Worker MCP registration form" },
|
|
759
957
|
{ id: "node-engine", label: "Expected Node engine" },
|
|
@@ -921,6 +1119,10 @@ export function mapConductorDoctorReportToReadinessChecks(report) {
|
|
|
921
1119
|
record(outcome);
|
|
922
1120
|
for (const outcome of outcomesOf(report?.local_merge))
|
|
923
1121
|
record(outcome);
|
|
1122
|
+
// BAPI-1135 — both observability outcomes flow through the same readiness-check
|
|
1123
|
+
// mapping as every other doctor outcome, so no surface formats them twice.
|
|
1124
|
+
for (const outcome of outcomesOf(report?.github_actions_merge))
|
|
1125
|
+
record(outcome);
|
|
924
1126
|
for (const outcome of outcomesOf(report?.native_ledger))
|
|
925
1127
|
record(outcome);
|
|
926
1128
|
record(denyEnforcementOutcome(report));
|
|
@@ -23,7 +23,7 @@ import { processGateMetMerge } from "./supervisor-merge.js";
|
|
|
23
23
|
import { observePrCiOnce } from "./pr-ci-producer.js";
|
|
24
24
|
import { runGhCommand } from "./pr-discovery.js";
|
|
25
25
|
import { isPrMergeConflict, parseGhPrMergeabilityFields, } from "./github-mergeability.js";
|
|
26
|
-
import { makeLocalMergeExecutor, resolveLocalMergeMethod } from "./local-merge.js";
|
|
26
|
+
import { makeLocalMergeExecutor, resolveLocalMergeCredential, resolveLocalMergeMethod, } from "./local-merge.js";
|
|
27
27
|
import { getHeadSha, getPrNumber } from "./event-accessors.js";
|
|
28
28
|
import { emitConductorEventIfNew } from "./producer-ledger.js";
|
|
29
29
|
import { rebuildObservedState, extractWorkerLiveness, } from "./epic-state.js";
|
|
@@ -1320,7 +1320,16 @@ export async function runEpicTick(options, deps = {}) {
|
|
|
1320
1320
|
merge: makeLocalMergeExecutor({
|
|
1321
1321
|
method: resolveLocalMergeMethod(localCfg.method),
|
|
1322
1322
|
approvalRequired: localCfg.approval_required === true,
|
|
1323
|
-
},
|
|
1323
|
+
},
|
|
1324
|
+
// BAPI-1136: `env` now supplies only the allowlisted
|
|
1325
|
+
// operational base; the identity is the explicitly configured
|
|
1326
|
+
// local credential, and an unconfigured one fails closed with
|
|
1327
|
+
// `local_merge_credential_missing` rather than borrowing the
|
|
1328
|
+
// ambient `gh` login this path used to rely on.
|
|
1329
|
+
{
|
|
1330
|
+
env: process.env,
|
|
1331
|
+
credential: resolveLocalMergeCredential(process.env, "local"),
|
|
1332
|
+
}),
|
|
1324
1333
|
// BAPI-525 Change A: durable attribution for a worker_id-less gate.met.
|
|
1325
1334
|
resolveWorkerIdForGateEvent,
|
|
1326
1335
|
});
|
|
@@ -36,7 +36,7 @@ import { buildConductorDoctorReport, formatConductorDoctorReport, } from "./doct
|
|
|
36
36
|
import { inspectBridgeApiProfileToken, } from "../mcp-host-config.js";
|
|
37
37
|
import { collectExecutorServiceDiagnostics, formatExecutorServiceDiagnosticsReport, } from "../doctor.js";
|
|
38
38
|
import { collectInstallStatusChecks, formatInstallStatusReport, } from "../install-doctor.js";
|
|
39
|
-
import { bringUpSafetySections, conductorHookTargetsSection, conductorLedgerLoadabilitySection, conductorProfileTokenSection, EPIC_IMPLEMENTER_PROFILE_TOKEN, detectConductorContext, executorObservationSection, executorProvisioningSection, executorServiceStateSection, githubActionsSection, githubCredentialsSection, inspectWorkflowPresence, reconcilerSection, supervisorSection, workflowSection, } from "./readiness-sections.js";
|
|
39
|
+
import { bringUpSafetySections, conductorHookTargetsSection, conductorLedgerLoadabilitySection, gitHubActionsMergeObservabilitySections, conductorProfileTokenSection, EPIC_IMPLEMENTER_PROFILE_TOKEN, detectConductorContext, executorObservationSection, executorProvisioningSection, executorServiceStateSection, githubActionsSection, mergeExecutionSections, githubCredentialsSection, inspectWorkflowPresence, reconcilerSection, supervisorSection, workflowSection, } from "./readiness-sections.js";
|
|
40
40
|
import { ConductorBridgeApiError, fetchConductorReadiness, } from "./bridge-api-client.js";
|
|
41
41
|
/**
|
|
42
42
|
* Run one optional collector, containing any throw.
|
|
@@ -157,6 +157,13 @@ export async function runConductorInstallDoctor(deps) {
|
|
|
157
157
|
sections.push(...supervisorSection(readiness));
|
|
158
158
|
sections.push(githubCredentialsSection(readiness));
|
|
159
159
|
sections.push(githubActionsSection(readiness));
|
|
160
|
+
// BAPI-1135 — the shared "Merge execution eligibility" section, immediately
|
|
161
|
+
// after the App permission row it is most often confused with, so the two
|
|
162
|
+
// write facts are read side by side. Built by `readiness-sections.ts` rather
|
|
163
|
+
// than formatted here, so this installer and the advisory gate render one
|
|
164
|
+
// classification; the installer's `settle`-based degraded isolation is
|
|
165
|
+
// unchanged, and no row here is `fatal`.
|
|
166
|
+
sections.push(...mergeExecutionSections(readiness));
|
|
160
167
|
}
|
|
161
168
|
const workflow = await inspectWorkflowPresence(deps.readWorkflowFile, deps.classifyWorkflowDrift);
|
|
162
169
|
sections.push(workflowSection(workflow.presence, deps.reviewPolicySource, workflow.driftDetail));
|
|
@@ -230,6 +237,13 @@ export async function runConductorInstallDoctor(deps) {
|
|
|
230
237
|
sections.push(conductorProfileTokenSection(profileToken, conductorContext));
|
|
231
238
|
sections.push(conductorHookTargetsSection(legacyConductor, conductorContext));
|
|
232
239
|
sections.push(conductorLedgerLoadabilitySection(legacyConductor));
|
|
240
|
+
// BAPI-1135 — this HOST's ability to observe the merge capability, beside the
|
|
241
|
+
// other locally-derived doctor rows. Distinct from the server-fact
|
|
242
|
+
// "Merge execution eligibility" section pushed above: that one says whether
|
|
243
|
+
// the repository can merge, these two say whether this machine can look.
|
|
244
|
+
// `settle`-based degraded isolation is unchanged — a doctor that failed to
|
|
245
|
+
// collect yields two explicit unknown rows, never an omitted section.
|
|
246
|
+
sections.push(...gitHubActionsMergeObservabilitySections(legacyConductor));
|
|
233
247
|
return {
|
|
234
248
|
legacyConductor,
|
|
235
249
|
legacyExecutorPreflight,
|
|
@@ -335,6 +349,14 @@ export function conductorInstallDoctorHasFatal(report) {
|
|
|
335
349
|
// remediation constants, or the installer reporting APIs from
|
|
336
350
|
// `conductor/install-doctor.js` keeps resolving exactly as before.
|
|
337
351
|
// ---------------------------------------------------------------------------
|
|
338
|
-
export { BRING_UP_SECTION_LABELS, bringUpSafetySections,
|
|
352
|
+
export { BRING_UP_SECTION_LABELS, bringUpSafetySections,
|
|
353
|
+
// BAPI-1135 — re-exported on the compatibility surface like every other
|
|
354
|
+
// section builder, so an existing importer of this module sees the new
|
|
355
|
+
// section without learning a second import path.
|
|
356
|
+
mergeExecutionSections, databaseMigrationCurrentSection, singleTransitionAuthoritySection, workerPresentSection, CONDUCTOR_OPERATOR_RUNBOOK_POINTER, EXECUTOR_PROVISIONING_GUIDANCE, EPIC_IMPLEMENTER_PROFILE_TOKEN, CONDUCTOR_PROFILE_TOKEN_NO_ENTRY_REMEDIATION, CONDUCTOR_PROFILE_TOKEN_REMEDIATION, CONDUCTOR_HOOK_TARGET_REMEDIATION, CONDUCTOR_LEDGER_LOADABILITY_REMEDIATION, EXECUTOR_INSTALL_SERVICE_REMEDIATION, EXECUTOR_SERVICE_START_REMEDIATION,
|
|
357
|
+
// BAPI-1135 — both new section builders join the compatibility surface like
|
|
358
|
+
// every other one, so an existing importer of this module does not learn a
|
|
359
|
+
// second import path for them.
|
|
360
|
+
gitHubActionsMergeObservabilitySections, } from "./readiness-sections.js";
|
|
339
361
|
// BAPI-1153 — the bring-up remediations the three sections above render.
|
|
340
362
|
export { BRING_UP_RUNBOOK, BRING_UP_UNREPORTED_REMEDIATION, DATABASE_MIGRATION_BEHIND_REMEDIATION, DATABASE_MIGRATION_UNREADABLE_REMEDIATION, SECOND_RECONCILER_REMEDIATION, WORKER_HEARTBEAT_UNREADABLE_REMEDIATION, WORKER_NOT_PRESENT_REMEDIATION, } from "./bring-up-facts.js";
|