@bridge_gpt/mcp-server 0.2.58 → 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 +231 -113
- package/build/agent-guidance.generated.js +1 -1
- package/build/agent-notes.js +7 -1
- package/build/agent-registry.js +15 -0
- package/build/agent-utils.js +16 -8
- package/build/claude-user-config-doctor.js +1 -35
- package/build/codex-bridge-profile.js +1 -1
- package/build/commands.generated.js +7 -8
- package/build/conductor/bridge-api-client.js +202 -2
- package/build/conductor/bring-up-facts.js +1 -1
- package/build/conductor/claude-hook.js +16 -16
- package/build/conductor/doctor.js +279 -72
- package/build/conductor/epic-implementer-cli.js +38 -5
- package/build/conductor/epic-reconcile.js +1 -1
- package/build/conductor/epic-runtime.js +21 -12
- package/build/conductor/errors.js +3 -3
- package/build/conductor/file-scope-guard.js +4 -4
- package/build/conductor/git-ci-types.js +1 -1
- package/build/conductor/git-inspection.js +2 -2
- package/build/conductor/install-doctor.js +28 -6
- 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/merge-ledger.js +2 -2
- package/build/conductor/pr-ci-producer.js +6 -6
- package/build/conductor/pr-review-producer.js +2 -2
- package/build/conductor/producer-ledger.js +2 -0
- package/build/conductor/readiness-sections.js +166 -9
- package/build/conductor/readiness.js +97 -16
- package/build/conductor/recovery-cli.js +243 -12
- package/build/conductor/recovery-operations.js +64 -6
- package/build/conductor/repair-contract.js +38 -4
- package/build/conductor/spec-review-producer.js +6 -6
- package/build/conductor/store.js +10 -10
- package/build/conductor/supervisor-config.js +14 -14
- package/build/conductor/supervisor-ledger.js +2 -2
- package/build/conductor/supervisor-message-relay.js +2 -2
- package/build/conductor/worker-ledger-cli.js +19 -19
- package/build/conductor-bundle-artifacts.js +2 -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 +199 -303
- package/build/drive-epic.js +88 -71
- package/build/epic-implementer-bin.js +22 -17
- package/build/epic-implementer-claude-hook-bin.js +4 -2
- package/build/epic-implementer-env.js +184 -0
- package/build/epic-integration-pr.js +2 -2
- 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 +79 -8
- package/build/executor/heartbeat.js +1 -1
- package/build/executor/index.js +21 -4
- package/build/executor/job-errors.js +3 -3
- package/build/executor/job-runner.js +127 -1
- package/build/executor/job-types.js +12 -2
- package/build/executor/merge-job.js +46 -5
- package/build/executor/observation.js +1 -1
- package/build/executor/preflight.js +1 -1
- package/build/executor/prompt-spec.js +1 -1
- package/build/executor/reconcile-overlap-job.js +10 -130
- package/build/executor/reconcile-overlap-types.js +1 -1
- package/build/executor/runner.js +1 -1
- 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 +320 -342
- package/build/init.js +20 -28
- package/build/install-bridge-conductor.js +208 -15
- package/build/install-bridge.js +555 -37
- package/build/mcp-host-entry-adapter.js +6 -0
- package/build/mcp-host-targets.js +2 -0
- package/build/mcp-install-state.js +42 -4
- package/build/mcp-invoke.js +5 -5
- package/build/mcp-profile.js +2 -2
- package/build/mcp-provisioning.js +2 -2
- package/build/mcp-server-invocation.js +3 -3
- package/build/merge-pull-request.js +118 -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/model-routing-host.js +25 -0
- package/build/model-routing-preferences.js +99 -0
- package/build/pipelines.generated.js +10 -10
- package/build/{plan-epic-conductor-eligibility.js → plan-epic-implementer-eligibility.js} +18 -16
- package/build/plan-execution-ledger.js +8 -0
- package/build/plane/cli.js +24 -2
- package/build/plane/member-roster.js +5 -5
- package/build/plane/preflight.js +68 -0
- package/build/plane/supervisor.js +1 -1
- package/build/plane/types.js +5 -5
- package/build/readiness-check.js +1 -1
- package/build/readme.generated.js +1 -1
- package/build/review-tickets.js +4 -4
- package/build/run-unit-tests-launcher.js +5 -0
- package/build/setup-epic.js +171 -17
- package/build/start-tickets-conductor.js +32 -32
- package/build/start-tickets-prereqs.js +4 -9
- package/build/start-tickets.js +475 -91
- package/build/version.generated.js +3 -3
- package/docs/CONDUCTOR.md +69 -18
- package/docs/install/mcp-tool-integrations.md +2 -1
- package/package.json +4 -4
- package/pipelines/implement-ticket.json +1 -1
- package/pipelines/plan-epic.json +2 -2
- 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/checkpoint-store.js +0 -542
- package/build/implement-epic/cli.js +0 -3158
- 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/{implement-epic → epic-shared}/bridge-client.js +0 -0
- /package/build/{implement-epic → epic-shared}/cut-protocol.js +0 -0
|
@@ -13,12 +13,13 @@ 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";
|
|
19
20
|
import { createReadinessCheck, createReadinessCheckSafely, } from "../readiness-check.js";
|
|
20
21
|
/**
|
|
21
|
-
* Remediation for a conductor context whose resolved MCP groups omit `
|
|
22
|
+
* Remediation for a conductor context whose resolved MCP groups omit `epic_implementer`.
|
|
22
23
|
*
|
|
23
24
|
* Fixed prose: it never interpolates the raw `BRIDGE_MCP_PROFILE` value. The
|
|
24
25
|
* legacy warning does interpolate it (unchanged), but a consolidated report is a
|
|
@@ -26,7 +27,7 @@ import { createReadinessCheck, createReadinessCheckSafely, } from "../readiness-
|
|
|
26
27
|
* gate needs to echo to name the fix. ADD, never replace — replacing the value
|
|
27
28
|
* would drop the worker's other groups.
|
|
28
29
|
*/
|
|
29
|
-
export const MCP_PROFILE_REMEDIATION = "ADD `
|
|
30
|
+
export const MCP_PROFILE_REMEDIATION = "ADD `epic_implementer` to BRIDGE_MCP_PROFILE (comma-separated) — do NOT replace the existing value, " +
|
|
30
31
|
"which would drop the worker's other tool groups. Without it the conductor/event/supervisor " +
|
|
31
32
|
"tools are not registered.";
|
|
32
33
|
/**
|
|
@@ -38,13 +39,24 @@ export const MCP_PROFILE_REMEDIATION = "ADD `conductor` to BRIDGE_MCP_PROFILE (c
|
|
|
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
|
*
|
|
@@ -70,9 +82,10 @@ export const MCP_PROFILE_UNSET = "(unset)";
|
|
|
70
82
|
/**
|
|
71
83
|
* The CLI-only verbs that call the Bridge API from a shell-spawned process and
|
|
72
84
|
* therefore resolve credentials themselves (CLAUDE.md "Shell-Spawned Credential
|
|
73
|
-
* Resolution"): the four BAPI-872 recovery verbs and the
|
|
74
|
-
* verbs. `.mcp.json` env is invisible to
|
|
75
|
-
* env and the user-scoped credential
|
|
85
|
+
* Resolution"): the four BAPI-872 recovery verbs and the six run-addressed repair
|
|
86
|
+
* verbs (BAPI-1154's three and BAPI-1176's three). `.mcp.json` env is invisible to
|
|
87
|
+
* them — only `BAPI_API_KEY` in the parent env and the user-scoped credential
|
|
88
|
+
* store count.
|
|
76
89
|
*/
|
|
77
90
|
export const CREDENTIALED_RECOVERY_COMMANDS = [
|
|
78
91
|
"stop-run",
|
|
@@ -82,6 +95,9 @@ export const CREDENTIALED_RECOVERY_COMMANDS = [
|
|
|
82
95
|
"scope-reparse",
|
|
83
96
|
"dispatch-remediation",
|
|
84
97
|
"merge-child",
|
|
98
|
+
"scope-catch-up",
|
|
99
|
+
"patch-policy",
|
|
100
|
+
"resume-run",
|
|
85
101
|
];
|
|
86
102
|
/** Remediation for recovery/repair verbs that cannot resolve Bridge credentials. */
|
|
87
103
|
export const REPAIR_CREDENTIAL_REMEDIATION = "export BAPI_API_KEY in the shell that runs `conductor`, or store a credential for this " +
|
|
@@ -364,13 +380,13 @@ export async function collectConductorNativeLedgerSafe(deps = {}) {
|
|
|
364
380
|
}
|
|
365
381
|
/**
|
|
366
382
|
* Inspect the BRIDGE_MCP_PROFILE environment variable and warn when a
|
|
367
|
-
* conductor/epic context is detected but the resolved groups omit `
|
|
383
|
+
* conductor/epic context is detected but the resolved groups omit `epic_implementer`.
|
|
368
384
|
* Strictly read-only, never throws.
|
|
369
385
|
*
|
|
370
386
|
* BAPI-772: health is judged from {@link resolveProfiles} — the SAME resolver the
|
|
371
387
|
* MCP server registers tools with — instead of comparing the raw string to
|
|
372
|
-
* `"
|
|
373
|
-
* `BRIDGE_MCP_PROFILE=
|
|
388
|
+
* `"epic_implementer"`. The old equality check called a worker running
|
|
389
|
+
* `BRIDGE_MCP_PROFILE=epic_implementer,sfcc` (exactly what `buildConductorWorkerEnv`
|
|
374
390
|
* produces on SFCC projects) or `full` degraded, and its remediation told the
|
|
375
391
|
* operator to REPLACE the variable — which would have destroyed the worker's
|
|
376
392
|
* other groups. The remediation now says ADD.
|
|
@@ -380,15 +396,15 @@ export function inspectMcpProfile(env) {
|
|
|
380
396
|
const raw_profile = raw === undefined ? MCP_PROFILE_UNSET : raw;
|
|
381
397
|
const groups = resolveProfiles(raw);
|
|
382
398
|
const active_groups = Array.from(groups);
|
|
383
|
-
const conductor_context_detected = env.
|
|
384
|
-
const
|
|
399
|
+
const conductor_context_detected = env.BAPI_EPIC_IMPLEMENTER_ENABLED === "1" || env.BAPI_EPIC_IMPLEMENTER_ENABLED === "true";
|
|
400
|
+
const retired_profile = raw?.split(",").some((token) => token.trim().toLowerCase() === "conductor") ?? false;
|
|
401
|
+
const degraded = retired_profile || (conductor_context_detected && !groups.has("epic_implementer"));
|
|
385
402
|
const warnings = [];
|
|
386
403
|
if (degraded) {
|
|
387
|
-
warnings.push(`
|
|
388
|
-
|
|
389
|
-
`
|
|
390
|
-
|
|
391
|
-
`Without it the conductor/event/supervisor tools are not registered.`);
|
|
404
|
+
warnings.push(`Epic Implementer MCP profile is inactive (active groups: ${active_groups.join(", ")}). ` +
|
|
405
|
+
(retired_profile ? "The retired conductor profile token is ignored. " : "") +
|
|
406
|
+
"ADD `epic_implementer` to BRIDGE_MCP_PROFILE (comma-separated); keep other valid groups. " +
|
|
407
|
+
"Without it the Epic Implementer tools are not registered.");
|
|
392
408
|
}
|
|
393
409
|
// Fixed state labels and the CLOSED active-group vocabulary only (BAPI-1055):
|
|
394
410
|
// `resolveProfiles` yields nothing but recognized group names, so this detail
|
|
@@ -397,14 +413,14 @@ export function inspectMcpProfile(env) {
|
|
|
397
413
|
degraded
|
|
398
414
|
? {
|
|
399
415
|
id: "mcp-profile",
|
|
400
|
-
label: "MCP
|
|
416
|
+
label: "Epic Implementer MCP profile",
|
|
401
417
|
status: "warn",
|
|
402
|
-
detail: `
|
|
418
|
+
detail: `the resolved groups omit \`epic_implementer\` (active: ${active_groups.join(", ")})`,
|
|
403
419
|
remediation: MCP_PROFILE_REMEDIATION,
|
|
404
420
|
}
|
|
405
421
|
: {
|
|
406
422
|
id: "mcp-profile",
|
|
407
|
-
label: "MCP
|
|
423
|
+
label: "Epic Implementer MCP profile",
|
|
408
424
|
status: "pass",
|
|
409
425
|
detail: conductor_context_detected
|
|
410
426
|
? `conductor context active; resolved groups: ${active_groups.join(", ")}`
|
|
@@ -418,7 +434,7 @@ export function inspectMcpProfile(env) {
|
|
|
418
434
|
* `gh auth status` (no writes, no merge) and reports whether the host could merge
|
|
419
435
|
* a PR locally if `policy_json.local_merge.enabled` were set. Never throws.
|
|
420
436
|
*/
|
|
421
|
-
export function inspectLocalMerge(runCommand) {
|
|
437
|
+
export function inspectLocalMerge(runCommand, env = process.env) {
|
|
422
438
|
const run = runCommand ??
|
|
423
439
|
((cmd, args) => {
|
|
424
440
|
try {
|
|
@@ -443,48 +459,51 @@ export function inspectLocalMerge(runCommand) {
|
|
|
443
459
|
}
|
|
444
460
|
});
|
|
445
461
|
let gh_available = false;
|
|
446
|
-
let gh_authed = false;
|
|
447
462
|
try {
|
|
448
463
|
gh_available = run("gh", ["--version"]).status === 0;
|
|
449
464
|
}
|
|
450
465
|
catch {
|
|
451
466
|
gh_available = false;
|
|
452
467
|
}
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
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);
|
|
461
480
|
const warnings = [];
|
|
462
481
|
if (!gh_available) {
|
|
463
482
|
warnings.push("`gh` is not installed or not on PATH. Local merge (policy_json.local_merge.enabled) " +
|
|
464
483
|
"cannot run; install the GitHub CLI to enable conductor-driven merges.");
|
|
465
484
|
}
|
|
466
|
-
else if (!
|
|
467
|
-
warnings.push("`gh` is installed but
|
|
468
|
-
"
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
//
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
//
|
|
475
|
-
//
|
|
476
|
-
//
|
|
477
|
-
//
|
|
478
|
-
//
|
|
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.
|
|
479
499
|
//
|
|
480
|
-
// AC-7
|
|
481
|
-
//
|
|
482
|
-
//
|
|
483
|
-
//
|
|
484
|
-
//
|
|
485
|
-
//
|
|
486
|
-
//
|
|
487
|
-
// 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.
|
|
488
507
|
const outcomes = [
|
|
489
508
|
gh_available
|
|
490
509
|
? {
|
|
@@ -502,28 +521,155 @@ export function inspectLocalMerge(runCommand) {
|
|
|
502
521
|
},
|
|
503
522
|
!gh_available
|
|
504
523
|
? {
|
|
505
|
-
id: "
|
|
506
|
-
label: "
|
|
524
|
+
id: "merge-credential",
|
|
525
|
+
label: "Configured merge credential",
|
|
507
526
|
status: "skip",
|
|
508
527
|
detail: GH_AUTH_SKIPPED_DETAIL,
|
|
509
528
|
remediation: GH_CLI_REMEDIATION,
|
|
510
529
|
}
|
|
511
|
-
:
|
|
530
|
+
: merge_credential_configured
|
|
512
531
|
? {
|
|
513
|
-
id: "
|
|
514
|
-
label: "
|
|
532
|
+
id: "merge-credential",
|
|
533
|
+
label: "Configured merge credential",
|
|
515
534
|
status: "pass",
|
|
516
|
-
detail: "
|
|
535
|
+
detail: "BAPI_LOCAL_MERGE_TOKEN or BAPI_MACHINE_MERGE_TOKEN is set",
|
|
517
536
|
}
|
|
518
537
|
: {
|
|
519
|
-
id: "
|
|
520
|
-
label: "
|
|
538
|
+
id: "merge-credential",
|
|
539
|
+
label: "Configured merge credential",
|
|
521
540
|
status: "warn",
|
|
522
|
-
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",
|
|
523
543
|
remediation: GH_AUTH_REMEDIATION,
|
|
524
544
|
},
|
|
525
545
|
];
|
|
526
|
-
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
|
+
};
|
|
527
673
|
}
|
|
528
674
|
/**
|
|
529
675
|
* Build the combined read-only doctor report. Composes the existing ledger
|
|
@@ -549,12 +695,43 @@ export async function buildConductorDoctorReport(deps = {}) {
|
|
|
549
695
|
git_hooks: inspectHooks(deps.hooksDeps),
|
|
550
696
|
mcp_profile,
|
|
551
697
|
local_merge: inspectLocalMerge(deps.runCommand),
|
|
698
|
+
github_actions_merge: inspectGitHubActionsMergeSafe(deps),
|
|
552
699
|
native_ledger,
|
|
553
700
|
deny_enforcement,
|
|
554
701
|
claude_login,
|
|
555
702
|
repair_credentials: await inspectRepairVerbCredentials(deps.env ?? process.env, deps.resolveRepairAccess),
|
|
556
703
|
};
|
|
557
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
|
+
}
|
|
558
735
|
/**
|
|
559
736
|
* Run the deny-enforcement inspector and convert any thrown exception into degraded
|
|
560
737
|
* report data — a deny-probe failure must never throw out of doctor. The underlying
|
|
@@ -594,7 +771,7 @@ async function inspectDenyEnforcementSafe(inspect) {
|
|
|
594
771
|
* hierarchy across sections.
|
|
595
772
|
*/
|
|
596
773
|
export function formatConductorDoctorReport(report) {
|
|
597
|
-
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;
|
|
598
775
|
const lines = [
|
|
599
776
|
"Conductor ledger doctor",
|
|
600
777
|
"───────────────────────",
|
|
@@ -648,6 +825,7 @@ export function formatConductorDoctorReport(report) {
|
|
|
648
825
|
// value of `full` is healthy precisely because it resolves to a set containing
|
|
649
826
|
// `conductor`.
|
|
650
827
|
const profileTag = mcp_profile.degraded ? "[WARNING] degraded" : "[OK]";
|
|
828
|
+
lines.push("Epic Implementer MCP profile");
|
|
651
829
|
lines.push(`raw profile: ${mcp_profile.raw_profile}`);
|
|
652
830
|
lines.push(`active groups: ${mcp_profile.active_groups.join(", ")} ${profileTag}`);
|
|
653
831
|
lines.push(`conductor context: ${mcp_profile.conductor_context_detected}`);
|
|
@@ -661,13 +839,38 @@ export function formatConductorDoctorReport(report) {
|
|
|
661
839
|
lines.push("Local merge capability (optional, opt-in)");
|
|
662
840
|
lines.push("─────────────────────────────────────────");
|
|
663
841
|
const ghTag = local_merge.gh_available
|
|
664
|
-
? local_merge.
|
|
842
|
+
? local_merge.merge_credential_configured
|
|
665
843
|
? "[OK]"
|
|
666
|
-
: "[WARNING]
|
|
844
|
+
: "[WARNING] no merge credential configured"
|
|
667
845
|
: "[WARNING] not installed";
|
|
668
846
|
lines.push(`gh available: ${local_merge.gh_available} ${ghTag}`);
|
|
669
|
-
lines.push(`
|
|
847
|
+
lines.push(`merge credential set: ${local_merge.merge_credential_configured}`);
|
|
670
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
|
+
}
|
|
671
874
|
if (local_merge.warnings.length > 0) {
|
|
672
875
|
lines.push("local merge warnings:");
|
|
673
876
|
for (const w of local_merge.warnings)
|
|
@@ -746,9 +949,9 @@ export function formatConductorDoctorReport(report) {
|
|
|
746
949
|
export const CONDUCTOR_READINESS_DESCRIPTORS = [
|
|
747
950
|
{ id: "ledger", label: "Conductor event ledger" },
|
|
748
951
|
{ id: "git-hooks", label: "Conductor git hooks" },
|
|
749
|
-
{ id: "mcp-profile", label: "MCP
|
|
952
|
+
{ id: "mcp-profile", label: "Epic Implementer MCP profile" },
|
|
750
953
|
{ id: "gh-cli", label: "GitHub CLI availability" },
|
|
751
|
-
{ id: "
|
|
954
|
+
{ id: "merge-credential", label: "Configured merge credential" },
|
|
752
955
|
{ id: "native-ledger", label: "Native ledger binding" },
|
|
753
956
|
{ id: "mcp-registration-form", label: "Worker MCP registration form" },
|
|
754
957
|
{ id: "node-engine", label: "Expected Node engine" },
|
|
@@ -916,6 +1119,10 @@ export function mapConductorDoctorReportToReadinessChecks(report) {
|
|
|
916
1119
|
record(outcome);
|
|
917
1120
|
for (const outcome of outcomesOf(report?.local_merge))
|
|
918
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);
|
|
919
1126
|
for (const outcome of outcomesOf(report?.native_ledger))
|
|
920
1127
|
record(outcome);
|
|
921
1128
|
record(denyEnforcementOutcome(report));
|
|
@@ -24,7 +24,7 @@ import { ConductorValidationError, ConductorEpicTickV1FrozenError, toConductorEr
|
|
|
24
24
|
import { emitConductorEvent, purgeConductorLedger, sendWorkerMessage, checkWorkerMessages, } from "./store.js";
|
|
25
25
|
import { isDuplicateConstraintError } from "./producer-ledger.js";
|
|
26
26
|
import { SEMANTIC_EVENT_TYPES } from "./taxonomy.js";
|
|
27
|
-
import { DRIVER_CONTEXT_MAX_BYTES } from "./repair-contract.js";
|
|
27
|
+
import { DRIVER_CONTEXT_MAX_BYTES, POLICY_PATCH_MAX_BYTES } from "./repair-contract.js";
|
|
28
28
|
import { installConductorGitHooks, resolveConductorHookBin } from "./git-hooks.js";
|
|
29
29
|
import { runFileScopeGuardCli } from "./file-scope-guard.js";
|
|
30
30
|
import { runPostCommitHookProducer, runReferenceTransactionHookProducer } from "./git-producer.js";
|
|
@@ -94,6 +94,15 @@ export function getConductorUsage() {
|
|
|
94
94
|
" merge-child --epic-run-id <id> --pr <number>",
|
|
95
95
|
" Merge the run's PR-bound child through the shared merge",
|
|
96
96
|
" service; the server composes the head and gate identity.",
|
|
97
|
+
" scope-catch-up --epic-run-id <id> --scope-id <scope> [--json]",
|
|
98
|
+
" Replay merges the run's OWN scope did not observe through the",
|
|
99
|
+
" guarded advance gate (no force). Refused if the scope is not the run's.",
|
|
100
|
+
" patch-policy --epic-run-id <id> --patch-file <path> [--json]",
|
|
101
|
+
" Merge-patch (RFC 7396) the run's policy on the server. Protected",
|
|
102
|
+
` paths refused; patch at most POLICY_PATCH_MAX_BYTES = ${POLICY_PATCH_MAX_BYTES} bytes.`,
|
|
103
|
+
" resume-run --epic-run-id <id> [--clear-quarantine] [--json]",
|
|
104
|
+
" Resume a paused run (a quarantined one only with --clear-quarantine);",
|
|
105
|
+
" activation is re-checked server-side. Active/no-control is a no-op.",
|
|
97
106
|
"",
|
|
98
107
|
"supervise options:",
|
|
99
108
|
" --run-id <id> Run/session identifier to supervise (required)",
|
|
@@ -168,10 +177,16 @@ export function getConductorUsage() {
|
|
|
168
177
|
" --raw-json '{\"branch\":\"feature/x\",\"sha\":\"abc123\"}'",
|
|
169
178
|
" epic-implementer emit-event --type ci.failed --source ci \\",
|
|
170
179
|
" --payload-ref 'file:///tmp/ci-log.txt'",
|
|
171
|
-
" epic-implementer emit-event --type merge.succeeded --source
|
|
180
|
+
" epic-implementer emit-event --type merge.succeeded --source epic-implementer-merge \\",
|
|
172
181
|
" --worker-id w1 --data-json '{\"summary\":\"auto-merged\",\"status\":\"succeeded\"}'",
|
|
173
182
|
" epic-implementer doctor --json",
|
|
174
183
|
" epic-implementer purge",
|
|
184
|
+
" epic-implementer scope-catch-up --epic-run-id <id> --scope-id <scope>",
|
|
185
|
+
" # a merge landed on epic/<KEY> that the scope never observed (unobserved_advance)",
|
|
186
|
+
" epic-implementer patch-policy --epic-run-id <id> --patch-file required-checks.json",
|
|
187
|
+
" # required_checks_empty_no_mode; the file holds {\"required_checks\":[\"conductor-ci / gate\"]}",
|
|
188
|
+
" epic-implementer resume-run --epic-run-id <id>",
|
|
189
|
+
" # a cap_wall_clock pause whose cause is now cleared",
|
|
175
190
|
"",
|
|
176
191
|
"epic-tick options:",
|
|
177
192
|
" --epic-key <KEY> Epic key to supervise (required, non-empty)",
|
|
@@ -263,6 +278,10 @@ const VALID_COMMANDS = new Set([
|
|
|
263
278
|
"scope-reparse",
|
|
264
279
|
"dispatch-remediation",
|
|
265
280
|
"merge-child",
|
|
281
|
+
// BAPI-1176: CLI-only run-addressed repair verbs — never registered as MCP tools.
|
|
282
|
+
"scope-catch-up",
|
|
283
|
+
"patch-policy",
|
|
284
|
+
"resume-run",
|
|
266
285
|
// Private, and deliberately ABSENT from the usage text: `__hook-bin` exists as
|
|
267
286
|
// the bundled-artifact regression guard for BAPI-772 (mirroring `plane
|
|
268
287
|
// __entrypoint`), not as a supported operator workflow.
|
|
@@ -833,7 +852,7 @@ export function parseEpicTickArgs(argv) {
|
|
|
833
852
|
/**
|
|
834
853
|
* Default the PreToolUse hook ON for epic-dispatched workers so the supervisor
|
|
835
854
|
* gets worker-liveness (`tool.intent`) signals without the operator having to
|
|
836
|
-
* export `
|
|
855
|
+
* export `BAPI_EPIC_IMPLEMENTER_ENABLE_PRE_TOOL_USE=1` (BAPI-441/A1 finding).
|
|
837
856
|
*
|
|
838
857
|
* Both the child-env copy and the hook registration key off this SAME
|
|
839
858
|
* parent-process env flag, read at dispatch time, so setting it once at the
|
|
@@ -843,8 +862,8 @@ export function parseEpicTickArgs(argv) {
|
|
|
843
862
|
* `process.env`) and returns it.
|
|
844
863
|
*/
|
|
845
864
|
export function applyEpicTickPreToolUseDefault(env = process.env) {
|
|
846
|
-
if (env.
|
|
847
|
-
env.
|
|
865
|
+
if (env.BAPI_EPIC_IMPLEMENTER_ENABLE_PRE_TOOL_USE === undefined) {
|
|
866
|
+
env.BAPI_EPIC_IMPLEMENTER_ENABLE_PRE_TOOL_USE = "1";
|
|
848
867
|
}
|
|
849
868
|
return env;
|
|
850
869
|
}
|
|
@@ -1281,6 +1300,20 @@ export async function runEpicImplementerCli(argv) {
|
|
|
1281
1300
|
const { runMergeChildCommand } = await import("./recovery-cli.js");
|
|
1282
1301
|
return await runMergeChildCommand(parsed.argv);
|
|
1283
1302
|
}
|
|
1303
|
+
// BAPI-1176: three more run-addressed repair verbs — CLI-only, lazily
|
|
1304
|
+
// imported like the rest, and dispatched before any MCP construction.
|
|
1305
|
+
case "scope-catch-up": {
|
|
1306
|
+
const { runScopeCatchUpCommand } = await import("./recovery-cli.js");
|
|
1307
|
+
return await runScopeCatchUpCommand(parsed.argv);
|
|
1308
|
+
}
|
|
1309
|
+
case "patch-policy": {
|
|
1310
|
+
const { runPatchPolicyCommand } = await import("./recovery-cli.js");
|
|
1311
|
+
return await runPatchPolicyCommand(parsed.argv);
|
|
1312
|
+
}
|
|
1313
|
+
case "resume-run": {
|
|
1314
|
+
const { runResumeRunCommand } = await import("./recovery-cli.js");
|
|
1315
|
+
return await runResumeRunCommand(parsed.argv);
|
|
1316
|
+
}
|
|
1284
1317
|
default:
|
|
1285
1318
|
console.error('Error: Unknown command. Run "epic-implementer --help" for usage.');
|
|
1286
1319
|
return 1;
|
|
@@ -55,7 +55,7 @@ export function safeDiagnosticMessage(err, fallback) {
|
|
|
55
55
|
*
|
|
56
56
|
* Deliberately diagnostic. The `epic_ticket_status` vocabulary — `parsing`,
|
|
57
57
|
* `needs_human` and the rest — is owned by the SERVER-side reconciler
|
|
58
|
-
* (`api/library/
|
|
58
|
+
* (`api/library/epic_implementer/reconciler.py`), which is the single transition
|
|
59
59
|
* authority for it and already parks this exact condition through its ordinary
|
|
60
60
|
* `_park_needs_human` commit point under the same `shadow_stale_deadline` reason.
|
|
61
61
|
* This pass runs the legacy supervisor's own, narrower status vocabulary, and
|