@bridge_gpt/mcp-server 0.2.41 → 0.2.43
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 +330 -191
- package/build/agent-capabilities/cli.js +2 -1
- package/build/agent-launchers/claude-executor-adapter.js +17 -4
- package/build/agents.generated.js +2 -2
- package/build/claude-review-workflow.js +510 -45
- package/build/claude-user-config-doctor.js +42 -11
- package/build/cli-release.js +2 -1
- package/build/commands.generated.js +6 -5
- package/build/conduct-epic/bridge-client.js +354 -113
- package/build/conduct-epic/checkpoint-store.js +17 -0
- package/build/conduct-epic/cli.js +947 -99
- package/build/conduct-epic/cut-protocol.js +327 -0
- package/build/conduct-epic/spawn.js +14 -2
- package/build/conductor/bridge-api-client.js +148 -1
- package/build/conductor/cli.js +109 -1
- package/build/conductor/doctor.js +101 -16
- package/build/conductor/epic-reconcile.js +72 -19
- package/build/conductor/epic-runtime.js +15 -3
- package/build/conductor/errors.js +47 -0
- package/build/conductor/git-hooks.js +205 -11
- package/build/conductor/install-doctor.js +230 -1
- package/build/conductor/local-merge.js +130 -28
- package/build/conductor/recovery-cli.js +313 -0
- package/build/conductor/recovery-operations.js +219 -0
- package/build/conductor/tools.js +32 -3
- package/build/conductor/worker-ledger-cli.js +27 -1
- package/build/conductor-bin.js +20 -16
- package/build/credentials-cli.js +3 -2
- package/build/docs.generated.js +2 -1
- package/build/doctor.js +120 -44
- package/build/drive-epic.js +375 -0
- package/build/executor/cli.js +48 -1
- package/build/executor/env.js +21 -0
- package/build/executor/http-client.js +71 -3
- package/build/executor/index-scope.js +39 -0
- package/build/executor/job-errors.js +9 -0
- package/build/executor/job-log-registry.js +69 -0
- package/build/executor/job-runner.js +198 -29
- package/build/executor/live-worker-registry.js +83 -0
- package/build/executor/observation.js +259 -6
- package/build/executor/platform.js +147 -3
- package/build/executor/process.js +58 -14
- package/build/executor/runner.js +454 -48
- package/build/executor/test-clock.js +3 -2
- package/build/executor/worker-finalization.js +233 -56
- package/build/executor/worktree.js +8 -1
- package/build/index-scope-contract.js +96 -0
- package/build/index.js +2277 -270
- package/build/init.js +83 -22
- package/build/install-bridge-conductor.js +323 -14
- package/build/install-bridge.js +225 -47
- package/build/install-doctor.js +23 -9
- package/build/install-reexec.js +2 -1
- package/build/launcher-config-inspection.js +83 -22
- package/build/mcp-host-config.js +331 -67
- package/build/mcp-host-targets.js +45 -21
- package/build/mcp-identity.js +92 -0
- package/build/mcp-install-state.js +94 -1
- package/build/mcp-invoke.js +2 -1
- package/build/mcp-provisioning.js +45 -12
- package/build/mcp-registration-doctor.js +35 -13
- package/build/mcp-server-invocation.js +4 -2
- package/build/merge-pull-request.js +208 -9
- package/build/pipelines.generated.js +305 -15
- package/build/plane/cli.js +73 -7
- package/build/plane/defaults.js +18 -5
- package/build/plane/manifest.js +90 -0
- package/build/plane/preflight.js +100 -10
- package/build/plane/shutdown.js +71 -3
- package/build/plane/test-fakes.js +9 -1
- package/build/readme.generated.js +1 -1
- package/build/regression-check.js +3 -2
- package/build/review-tickets.js +8 -7
- package/build/run-unit-tests-launcher.js +149 -6
- package/build/schedule-run.js +3 -2
- package/build/setup-epic.js +531 -82
- package/build/sfcc/tool-wrapper.js +15 -0
- package/build/start-tickets-prereqs.js +11 -6
- package/build/start-tickets.js +91 -85
- package/build/update-check.js +3 -2
- package/build/upgrade-advice.js +2 -1
- package/build/upgrade-cli.js +50 -18
- package/build/version.generated.js +2 -1
- package/build/worktree-core.js +31 -17
- package/docs/CONDUCTOR.md +22 -0
- package/docs/install/mcp-tool-integrations.md +19 -3
- package/package.json +2 -2
- package/pipelines/greenfield-setup.json +286 -0
package/build/conductor/cli.js
CHANGED
|
@@ -18,7 +18,7 @@ import { ConductorValidationError, ConductorEpicTickV1FrozenError, toConductorEr
|
|
|
18
18
|
import { emitConductorEvent, purgeConductorLedger, sendWorkerMessage, checkWorkerMessages, } from "./store.js";
|
|
19
19
|
import { isDuplicateConstraintError } from "./producer-ledger.js";
|
|
20
20
|
import { SEMANTIC_EVENT_TYPES } from "./taxonomy.js";
|
|
21
|
-
import { installConductorGitHooks } from "./git-hooks.js";
|
|
21
|
+
import { installConductorGitHooks, resolveConductorHookBin } from "./git-hooks.js";
|
|
22
22
|
import { runFileScopeGuardCli } from "./file-scope-guard.js";
|
|
23
23
|
import { runPostCommitHookProducer, runReferenceTransactionHookProducer } from "./git-producer.js";
|
|
24
24
|
import { buildConductorDoctorReport, formatConductorDoctorReport } from "./doctor.js";
|
|
@@ -65,6 +65,15 @@ export function getConductorUsage() {
|
|
|
65
65
|
" Run the reference-transaction producer (invoked by the hook)",
|
|
66
66
|
" file-scope-guard Warn-only: compare the branch diff against the declared",
|
|
67
67
|
" touched-file set (always exits 0; never blocks a PR)",
|
|
68
|
+
" stop-run Stop an epic run: block dispatch and cancel queued work",
|
|
69
|
+
" (the run record is preserved). Idempotent.",
|
|
70
|
+
" abandon-run Abandon an epic run — TERMINAL and IRREVERSIBLE.",
|
|
71
|
+
" Idempotent on an already-abandoned run.",
|
|
72
|
+
" unpark Move a parked (needs_human) ticket back into its gate",
|
|
73
|
+
" machine. Retries internally on a concurrent change.",
|
|
74
|
+
" adopt-current-head-and-unpark",
|
|
75
|
+
" Recover a ticket parked by a PR-head drift: adopt the",
|
|
76
|
+
" CURRENT PR head and unpark in one step.",
|
|
68
77
|
"",
|
|
69
78
|
"supervise options:",
|
|
70
79
|
" --run-id <id> Run/session identifier to supervise (required)",
|
|
@@ -157,8 +166,47 @@ export function getConductorUsage() {
|
|
|
157
166
|
" conductor approve-plan EPIC-405 --plan-version 2 --json",
|
|
158
167
|
" conductor epic-status --epic-key EPIC-405",
|
|
159
168
|
" conductor epic-status --epic-key EPIC-405 --json",
|
|
169
|
+
"",
|
|
170
|
+
"stop-run / abandon-run options:",
|
|
171
|
+
" --epic-run-id <id> Epic run identifier (required)",
|
|
172
|
+
" --json Print compact JSON result",
|
|
173
|
+
" --help Print this usage message",
|
|
174
|
+
" stop-run BLOCKS new dispatch and CANCELS queued work; the run record is",
|
|
175
|
+
" preserved and the run cannot resume on its own — it is an emergency brake,",
|
|
176
|
+
" not a pause. abandon-run is the SAME plus a TERMINAL, IRREVERSIBLE close-out:",
|
|
177
|
+
" once abandoned, the run can never resume or be reused. Both are idempotent —",
|
|
178
|
+
" repeating either is a safe no-op once the run is already stopped/abandoned.",
|
|
179
|
+
"",
|
|
180
|
+
"unpark / adopt-current-head-and-unpark options:",
|
|
181
|
+
" --epic-run-id <id> Epic run identifier (required)",
|
|
182
|
+
" --ticket-key <key> Ticket key (required)",
|
|
183
|
+
" --json Print compact JSON result",
|
|
184
|
+
" --help Print this usage message",
|
|
185
|
+
" unpark resumes a ticket PARKED with needs_human, once the underlying cause",
|
|
186
|
+
" is resolved. adopt-current-head-and-unpark additionally REBINDS the ticket's",
|
|
187
|
+
" anchored PR head to whatever the PR's head currently is, for a ticket parked",
|
|
188
|
+
" by a human/external push. Both retry internally on a concurrent row change —",
|
|
189
|
+
" neither accepts, nor ever asks for, a version counter.",
|
|
190
|
+
"",
|
|
191
|
+
"Incident examples:",
|
|
192
|
+
" # A run is misbehaving and must stop immediately:",
|
|
193
|
+
" conductor stop-run --epic-run-id 3f9c2b7e-...",
|
|
194
|
+
" # The run is done for good — close it out:",
|
|
195
|
+
" conductor abandon-run --epic-run-id 3f9c2b7e-...",
|
|
196
|
+
" # A ticket parked itself (needs_human) and the cause is now fixed:",
|
|
197
|
+
" conductor unpark --epic-run-id 3f9c2b7e-... --ticket-key BAPI-852",
|
|
198
|
+
" # A human pushed directly to the PR and the ticket parked on head drift:",
|
|
199
|
+
" conductor adopt-current-head-and-unpark --epic-run-id 3f9c2b7e-... --ticket-key BAPI-852",
|
|
160
200
|
].join("\n");
|
|
161
201
|
}
|
|
202
|
+
/**
|
|
203
|
+
* Private diagnostic action (BAPI-772): prints the `conductor-bin.js` path the
|
|
204
|
+
* EXECUTING artifact resolves for hook installation. It exists so a test can run
|
|
205
|
+
* `node build/conductor-bin.js __hook-bin` against the real esbuild bundle and
|
|
206
|
+
* assert the printed path exists — a tsc-emit unit test structurally cannot catch
|
|
207
|
+
* the bundled-layout bug this ticket fixes. Mirrors `plane __entrypoint`.
|
|
208
|
+
*/
|
|
209
|
+
export const CONDUCTOR_HOOK_BIN_ACTION = "__hook-bin";
|
|
162
210
|
const VALID_COMMANDS = new Set([
|
|
163
211
|
"emit-event",
|
|
164
212
|
"supervise",
|
|
@@ -172,6 +220,15 @@ const VALID_COMMANDS = new Set([
|
|
|
172
220
|
"install-git-hooks",
|
|
173
221
|
"git-hook",
|
|
174
222
|
"file-scope-guard",
|
|
223
|
+
// BAPI-872: CLI-only operator recovery verbs — never registered as MCP tools.
|
|
224
|
+
"stop-run",
|
|
225
|
+
"abandon-run",
|
|
226
|
+
"unpark",
|
|
227
|
+
"adopt-current-head-and-unpark",
|
|
228
|
+
// Private, and deliberately ABSENT from the usage text: `__hook-bin` exists as
|
|
229
|
+
// the bundled-artifact regression guard for BAPI-772 (mirroring `plane
|
|
230
|
+
// __entrypoint`), not as a supported operator workflow.
|
|
231
|
+
CONDUCTOR_HOOK_BIN_ACTION,
|
|
175
232
|
]);
|
|
176
233
|
/**
|
|
177
234
|
* Parse the top-level conductor argv into a subcommand (without a CLI
|
|
@@ -583,6 +640,11 @@ export async function runDoctorCommand(argv, deps = {}) {
|
|
|
583
640
|
* Run `install-git-hooks`: install/update the local managed `post-commit` and
|
|
584
641
|
* `reference-transaction` hooks. Returns 0 even when the directory is not a git
|
|
585
642
|
* worktree (degraded optional capability) — never a fatal failure.
|
|
643
|
+
*
|
|
644
|
+
* BAPI-772: an unresolvable `conductor-bin.js` IS fatal (exit 1). Writing a hook
|
|
645
|
+
* that points at a nonexistent binary produced a hook that silently did nothing
|
|
646
|
+
* on every commit, so the installer now refuses and this command reports the
|
|
647
|
+
* refusal with the resolver's searched-layout remediation.
|
|
586
648
|
*/
|
|
587
649
|
export function runInstallGitHooksCommand(argv) {
|
|
588
650
|
const { bools } = tokenizeFlags(argv, new Set(), DIAGNOSTIC_BOOL_FLAGS);
|
|
@@ -591,6 +653,14 @@ export function runInstallGitHooksCommand(argv) {
|
|
|
591
653
|
return 0;
|
|
592
654
|
}
|
|
593
655
|
const result = installConductorGitHooks();
|
|
656
|
+
if (!result.ok) {
|
|
657
|
+
if (bools.has("--json")) {
|
|
658
|
+
console.error(JSON.stringify({ error: result.error, reason: result.reason }));
|
|
659
|
+
return 1;
|
|
660
|
+
}
|
|
661
|
+
console.error(`Error: conductor git hooks were NOT installed (${result.error}): ${result.reason}`);
|
|
662
|
+
return 1;
|
|
663
|
+
}
|
|
594
664
|
if (bools.has("--json")) {
|
|
595
665
|
console.log(JSON.stringify(result));
|
|
596
666
|
return 0;
|
|
@@ -600,6 +670,7 @@ export function runInstallGitHooksCommand(argv) {
|
|
|
600
670
|
"───────────────────────────",
|
|
601
671
|
`is git worktree: ${result.is_worktree}`,
|
|
602
672
|
`hooks dir: ${result.hooks_dir ?? "n/a"}`,
|
|
673
|
+
`conductor bin: ${result.conductor_bin}`,
|
|
603
674
|
];
|
|
604
675
|
for (const hook of result.installed) {
|
|
605
676
|
lines.push(` ${hook.name}: ${hook.action}${hook.warning ? ` (${hook.warning})` : ""}`);
|
|
@@ -612,6 +683,21 @@ export function runInstallGitHooksCommand(argv) {
|
|
|
612
683
|
console.log(lines.join("\n"));
|
|
613
684
|
return 0;
|
|
614
685
|
}
|
|
686
|
+
/**
|
|
687
|
+
* Run the private `__hook-bin` diagnostic: resolve the hook binary from the
|
|
688
|
+
* executing artifact and print ONLY that canonical path to stdout, so artifact-
|
|
689
|
+
* level automation can consume it directly. A failed resolution prints the
|
|
690
|
+
* sanitized resolver reason to stderr and returns 1 — never a stack trace.
|
|
691
|
+
*/
|
|
692
|
+
export function runHookBinDiagnosticCommand() {
|
|
693
|
+
const resolution = resolveConductorHookBin();
|
|
694
|
+
if (!resolution.ok) {
|
|
695
|
+
console.error(`conductor hook bin UNRESOLVED: ${resolution.reason}`);
|
|
696
|
+
return 1;
|
|
697
|
+
}
|
|
698
|
+
console.log(resolution.path);
|
|
699
|
+
return 0;
|
|
700
|
+
}
|
|
615
701
|
const GIT_HOOK_VALUE_FLAGS = new Set(["--phase", "--stdin-file"]);
|
|
616
702
|
const GIT_HOOK_BOOL_FLAGS = new Set(["--help"]);
|
|
617
703
|
/**
|
|
@@ -1104,9 +1190,31 @@ export async function runConductorCli(argv) {
|
|
|
1104
1190
|
return runInstallGitHooksCommand(parsed.argv);
|
|
1105
1191
|
case "git-hook":
|
|
1106
1192
|
return await runGitHookCommand(parsed.argv);
|
|
1193
|
+
case CONDUCTOR_HOOK_BIN_ACTION:
|
|
1194
|
+
// Dispatched before any MCP server construction or store access: the
|
|
1195
|
+
// guard must be able to run against a bundle whose ledger is absent.
|
|
1196
|
+
return runHookBinDiagnosticCommand();
|
|
1107
1197
|
case "file-scope-guard":
|
|
1108
1198
|
// BAPI-507 (N-2): warn-only worker file-scope guard. Always exits 0.
|
|
1109
1199
|
return runFileScopeGuardCli();
|
|
1200
|
+
case "stop-run": {
|
|
1201
|
+
// BAPI-872: lazily imported so the Bridge credential/HTTP graph never
|
|
1202
|
+
// loads for a local-only command (doctor, emit-event, etc.).
|
|
1203
|
+
const { runStopRunCommand } = await import("./recovery-cli.js");
|
|
1204
|
+
return await runStopRunCommand(parsed.argv);
|
|
1205
|
+
}
|
|
1206
|
+
case "abandon-run": {
|
|
1207
|
+
const { runAbandonRunCommand } = await import("./recovery-cli.js");
|
|
1208
|
+
return await runAbandonRunCommand(parsed.argv);
|
|
1209
|
+
}
|
|
1210
|
+
case "unpark": {
|
|
1211
|
+
const { runUnparkCommand } = await import("./recovery-cli.js");
|
|
1212
|
+
return await runUnparkCommand(parsed.argv);
|
|
1213
|
+
}
|
|
1214
|
+
case "adopt-current-head-and-unpark": {
|
|
1215
|
+
const { runAdoptCurrentHeadAndUnparkCommand } = await import("./recovery-cli.js");
|
|
1216
|
+
return await runAdoptCurrentHeadAndUnparkCommand(parsed.argv);
|
|
1217
|
+
}
|
|
1110
1218
|
default:
|
|
1111
1219
|
console.error('Error: Unknown command. Run "conductor --help" for usage.');
|
|
1112
1220
|
return 1;
|
|
@@ -15,6 +15,10 @@ import { doctorConductorLedger } from "./store.js";
|
|
|
15
15
|
import { detectClaudeLogin, formatClaudeLoginAdvisory } from "../claude-login.js";
|
|
16
16
|
import { inspectConductorGitHooks } from "./git-hooks.js";
|
|
17
17
|
import { resolveMcpShimInvocationForRuntime, resolvePackageRootFromModuleUrl, } from "../mcp-server-invocation.js";
|
|
18
|
+
import { MCP_PACKAGE_NAME } from "../mcp-identity.js";
|
|
19
|
+
import { resolveProfiles } from "../mcp-profile.js";
|
|
20
|
+
/** Explicit display value for an unset `BRIDGE_MCP_PROFILE`. */
|
|
21
|
+
export const MCP_PROFILE_UNSET = "(unset)";
|
|
18
22
|
/**
|
|
19
23
|
* Explicit not-run state for the `deny_enforcement` section when the probe is
|
|
20
24
|
* skipped via `--no-deny-probe`. Enforcement is UNVERIFIED, so the state is
|
|
@@ -115,6 +119,66 @@ export async function inspectNativeLedger(nativeLoad, deps = {}) {
|
|
|
115
119
|
warnings,
|
|
116
120
|
};
|
|
117
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* Collect the ledger doctor result and its native-ledger inspection together.
|
|
124
|
+
*
|
|
125
|
+
* READ-ONLY and diagnostic only: it never installs or rebuilds
|
|
126
|
+
* `better-sqlite3`, opens a write transaction, creates or migrates the ledger,
|
|
127
|
+
* or alters package state. It may THROW when an injected `doctorLedger` seam
|
|
128
|
+
* throws — {@link collectConductorNativeLedgerSafe} is the containment wrapper
|
|
129
|
+
* for callers that must not see an exception.
|
|
130
|
+
*/
|
|
131
|
+
export async function collectConductorNativeLedger(deps = {}) {
|
|
132
|
+
const doctorLedger = deps.doctorLedger ?? doctorConductorLedger;
|
|
133
|
+
const ledger = await doctorLedger();
|
|
134
|
+
const native_ledger = await inspectNativeLedger(ledger.native_load, deps);
|
|
135
|
+
return { ledger, native_ledger };
|
|
136
|
+
}
|
|
137
|
+
/** Map a native-ledger inspection onto the reportable availability shape. */
|
|
138
|
+
export function describeNativeLedgerAvailability(inspection) {
|
|
139
|
+
if (inspection.binding_loads)
|
|
140
|
+
return { status: "available" };
|
|
141
|
+
return {
|
|
142
|
+
status: "degraded",
|
|
143
|
+
module: inspection.module,
|
|
144
|
+
nodeModulesAbi: inspection.node_modules_abi,
|
|
145
|
+
failureKind: inspection.failure_kind,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Sanitized failure-kind token used when the collection itself could not run.
|
|
150
|
+
*
|
|
151
|
+
* A collection failure and a load failure are different facts, so this is its
|
|
152
|
+
* own token rather than a reused one.
|
|
153
|
+
*/
|
|
154
|
+
export const NATIVE_LEDGER_UNCOLLECTED_FAILURE_KIND = "inspection_unavailable";
|
|
155
|
+
/**
|
|
156
|
+
* Collect the native-ledger inspection, containing every failure.
|
|
157
|
+
*
|
|
158
|
+
* A thrown collection error yields a GENERIC degraded inspection — no exception
|
|
159
|
+
* message, no absolute package path, no stack trace. An operator learns that the
|
|
160
|
+
* probe could not answer, which is the actionable fact; the raw error is
|
|
161
|
+
* discarded because it can carry a filesystem path.
|
|
162
|
+
*/
|
|
163
|
+
export async function collectConductorNativeLedgerSafe(deps = {}) {
|
|
164
|
+
try {
|
|
165
|
+
return (await collectConductorNativeLedger(deps)).native_ledger;
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
return {
|
|
169
|
+
cli_node_version: process.version,
|
|
170
|
+
engines_node: null,
|
|
171
|
+
mcp_shim_node: null,
|
|
172
|
+
mcp_registration_form: "unknown",
|
|
173
|
+
module: "better-sqlite3",
|
|
174
|
+
node_modules_abi: process.versions.modules,
|
|
175
|
+
binding_loads: false,
|
|
176
|
+
failure_kind: NATIVE_LEDGER_UNCOLLECTED_FAILURE_KIND,
|
|
177
|
+
degraded: true,
|
|
178
|
+
warnings: ["the native ledger inspection could not be collected on this host"],
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
}
|
|
118
182
|
/**
|
|
119
183
|
* Inspect the local schedule metadata store for a registered epic-tick schedule.
|
|
120
184
|
* Strictly read-only: composes orchestrateScheduleList (read-only list path) and
|
|
@@ -174,10 +238,10 @@ export async function inspectEpicTickSchedule(deps, orchestrateListOverride) {
|
|
|
174
238
|
warnings: [
|
|
175
239
|
"An epic-tick schedule is registered, but the v1 `conductor epic-tick` " +
|
|
176
240
|
"path is frozen (EPIC_TICK_V1_FROZEN) — it advances nothing. Cancel it: " +
|
|
177
|
-
|
|
241
|
+
`\`npx -y ${MCP_PACKAGE_NAME} schedule-run cancel --id ` +
|
|
178
242
|
`${entry.metadata.id ?? "<id>"}\`. ` +
|
|
179
243
|
"Epic Conductor v2 reconciles server-side; run jobs locally with " +
|
|
180
|
-
|
|
244
|
+
`\`npx -y ${MCP_PACKAGE_NAME} executor --repo <name>\`.`,
|
|
181
245
|
],
|
|
182
246
|
};
|
|
183
247
|
}
|
|
@@ -195,22 +259,35 @@ export async function inspectEpicTickSchedule(deps, orchestrateListOverride) {
|
|
|
195
259
|
}
|
|
196
260
|
/**
|
|
197
261
|
* Inspect the BRIDGE_MCP_PROFILE environment variable and warn when a
|
|
198
|
-
* conductor/epic context is detected but the
|
|
262
|
+
* conductor/epic context is detected but the resolved groups omit `conductor`.
|
|
199
263
|
* Strictly read-only, never throws.
|
|
264
|
+
*
|
|
265
|
+
* BAPI-772: health is judged from {@link resolveProfiles} — the SAME resolver the
|
|
266
|
+
* MCP server registers tools with — instead of comparing the raw string to
|
|
267
|
+
* `"conductor"`. The old equality check called a worker running
|
|
268
|
+
* `BRIDGE_MCP_PROFILE=conductor,sfcc` (exactly what `buildConductorWorkerEnv`
|
|
269
|
+
* produces on SFCC projects) or `full` degraded, and its remediation told the
|
|
270
|
+
* operator to REPLACE the variable — which would have destroyed the worker's
|
|
271
|
+
* other groups. The remediation now says ADD.
|
|
200
272
|
*/
|
|
201
273
|
export function inspectMcpProfile(env, epicTick) {
|
|
202
|
-
const
|
|
274
|
+
const raw = env.BRIDGE_MCP_PROFILE;
|
|
275
|
+
const raw_profile = raw === undefined ? MCP_PROFILE_UNSET : raw;
|
|
276
|
+
const groups = resolveProfiles(raw);
|
|
277
|
+
const active_groups = Array.from(groups);
|
|
203
278
|
const conductor_context_detected = env.BAPI_CONDUCTOR_ENABLED === "1" ||
|
|
204
279
|
env.BAPI_CONDUCTOR_ENABLED === "true" ||
|
|
205
280
|
epicTick.registered;
|
|
206
|
-
const degraded = conductor_context_detected &&
|
|
281
|
+
const degraded = conductor_context_detected && !groups.has("conductor");
|
|
207
282
|
const warnings = [];
|
|
208
283
|
if (degraded) {
|
|
209
|
-
warnings.push(`BRIDGE_MCP_PROFILE is
|
|
210
|
-
`
|
|
211
|
-
`
|
|
284
|
+
warnings.push(`BRIDGE_MCP_PROFILE is ${raw_profile} (active groups: ${active_groups.join(", ")}) ` +
|
|
285
|
+
`but a conductor context is active. ADD \`conductor\` to BRIDGE_MCP_PROFILE ` +
|
|
286
|
+
`(e.g. \`${raw_profile === MCP_PROFILE_UNSET ? "conductor" : `${raw_profile},conductor`}\`) — ` +
|
|
287
|
+
`do NOT replace the value, which would drop the worker's other groups. ` +
|
|
288
|
+
`Without it the conductor/event/supervisor tools are not registered.`);
|
|
212
289
|
}
|
|
213
|
-
return {
|
|
290
|
+
return { raw_profile, active_groups, conductor_context_detected, degraded, warnings };
|
|
214
291
|
}
|
|
215
292
|
/**
|
|
216
293
|
* Read-only probe of the local-merge (F4) capability. Runs `gh --version` and
|
|
@@ -277,13 +354,13 @@ export function inspectLocalMerge(runCommand) {
|
|
|
277
354
|
* Never mutates the ledger, the hooks, the schema, or the OS scheduler.
|
|
278
355
|
*/
|
|
279
356
|
export async function buildConductorDoctorReport(deps = {}) {
|
|
280
|
-
const doctorLedger = deps.doctorLedger ?? doctorConductorLedger;
|
|
281
357
|
const inspectHooks = deps.inspectHooks ?? inspectConductorGitHooks;
|
|
282
358
|
const epicTick = await inspectEpicTickSchedule(deps.scheduleDeps, deps.orchestrateList);
|
|
283
359
|
const mcp_profile = inspectMcpProfile(deps.env ?? process.env, epicTick);
|
|
284
|
-
// Resolve the ledger once so the native-load probe reflects the same report
|
|
285
|
-
|
|
286
|
-
|
|
360
|
+
// Resolve the ledger once so the native-load probe reflects the same report —
|
|
361
|
+
// through the SHARED collector (BAPI-775), so the installer's read-only probe
|
|
362
|
+
// and this report cannot diverge in their interpretation of the binding.
|
|
363
|
+
const { ledger, native_ledger } = await collectConductorNativeLedger(deps);
|
|
287
364
|
const deny_enforcement = deps.skipDenyProbe
|
|
288
365
|
? skippedDenyEnforcementResult()
|
|
289
366
|
: await inspectDenyEnforcementSafe(deps.inspectDenyEnforcement);
|
|
@@ -369,7 +446,11 @@ export function formatConductorDoctorReport(report) {
|
|
|
369
446
|
lines.push(`hooks dir: ${git_hooks.hooks_dir ?? "n/a"}`);
|
|
370
447
|
lines.push(`degraded: ${git_hooks.degraded}`);
|
|
371
448
|
for (const hook of git_hooks.hooks) {
|
|
372
|
-
|
|
449
|
+
// BAPI-772: the embedded target is shown alongside the structural fields —
|
|
450
|
+
// a managed block whose binary no longer exists is a DEAD hook, and this line
|
|
451
|
+
// is where that becomes visible. Advisory only: never changes the exit code.
|
|
452
|
+
lines.push(` ${hook.name}: exists=${hook.exists} executable=${hook.executable} managed=${hook.managed_block_present} ` +
|
|
453
|
+
`bin=${hook.embedded_bin_path ?? "n/a"} bin_is_file=${hook.embedded_bin_is_file}`);
|
|
373
454
|
}
|
|
374
455
|
if (git_hooks.warnings.length > 0) {
|
|
375
456
|
lines.push("git hook warnings:");
|
|
@@ -399,12 +480,16 @@ export function formatConductorDoctorReport(report) {
|
|
|
399
480
|
lines.push("");
|
|
400
481
|
lines.push("Epic Conductor v2 reconciles epics server-side — nothing to schedule");
|
|
401
482
|
lines.push("locally. To execute claimed jobs on this machine, run:");
|
|
402
|
-
lines.push(
|
|
483
|
+
lines.push(` npx -y ${MCP_PACKAGE_NAME} executor --repo <name>`);
|
|
403
484
|
lines.push("");
|
|
404
485
|
lines.push("MCP Profile (optional, local)");
|
|
405
486
|
lines.push("─────────────────────────────");
|
|
487
|
+
// The tag belongs to the RESOLVED interpretation, not the raw string: a raw
|
|
488
|
+
// value of `full` is healthy precisely because it resolves to a set containing
|
|
489
|
+
// `conductor`.
|
|
406
490
|
const profileTag = mcp_profile.degraded ? "[WARNING] degraded" : "[OK]";
|
|
407
|
-
lines.push(`
|
|
491
|
+
lines.push(`raw profile: ${mcp_profile.raw_profile}`);
|
|
492
|
+
lines.push(`active groups: ${mcp_profile.active_groups.join(", ")} ${profileTag}`);
|
|
408
493
|
lines.push(`conductor context: ${mcp_profile.conductor_context_detected}`);
|
|
409
494
|
lines.push(`degraded: ${mcp_profile.degraded}`);
|
|
410
495
|
if (mcp_profile.warnings.length > 0) {
|
|
@@ -19,6 +19,19 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import { computeReadySet, decideRemediation, DEFAULT_MAX_SPEC_REVIEW_ATTEMPTS } from "./epic-state.js";
|
|
21
21
|
import { extractMergeActionIdentityFromGateEvent } from "./merge-ledger.js";
|
|
22
|
+
/**
|
|
23
|
+
* The canonical park reason for a stale hold that ran out its deadline.
|
|
24
|
+
*
|
|
25
|
+
* Byte-identical to the Python reconciler's `SHADOW_STALE_DEADLINE_REASON`, and
|
|
26
|
+
* that is the point: both conductors park for the same cause, so an operator
|
|
27
|
+
* greps one string. This introduces no `epic_ticket_status` value — the ticket
|
|
28
|
+
* moves to the EXISTING `needs_human`, whose vocabulary is closed (R14).
|
|
29
|
+
*/
|
|
30
|
+
export const SHADOW_STALE_DEADLINE_REASON = "shadow_stale_deadline";
|
|
31
|
+
/** Normalize either accepted seam shape into the richer one. */
|
|
32
|
+
function normalizeShadowDecision(raw) {
|
|
33
|
+
return typeof raw === "string" ? { verdict: raw } : raw;
|
|
34
|
+
}
|
|
22
35
|
/**
|
|
23
36
|
* Sanitize a thrown value into a single-line, bounded diagnostic string for a
|
|
24
37
|
* tick warning (BAPI-487 Requirement 2). Surfaces the real `err.message` (the
|
|
@@ -37,6 +50,31 @@ export function safeDiagnosticMessage(err, fallback) {
|
|
|
37
50
|
// ---------------------------------------------------------------------------
|
|
38
51
|
// reconcileEpic
|
|
39
52
|
// ---------------------------------------------------------------------------
|
|
53
|
+
/**
|
|
54
|
+
* Surface an elapsed scope-freshness deadline without transitioning the ticket.
|
|
55
|
+
*
|
|
56
|
+
* Deliberately diagnostic. The `epic_ticket_status` vocabulary — `parsing`,
|
|
57
|
+
* `needs_human` and the rest — is owned by the SERVER-side reconciler
|
|
58
|
+
* (`api/library/epic_conductor/reconciler.py`), which is the single transition
|
|
59
|
+
* authority for it and already parks this exact condition through its ordinary
|
|
60
|
+
* `_park_needs_human` commit point under the same `shadow_stale_deadline` reason.
|
|
61
|
+
* This pass runs the legacy supervisor's own, narrower status vocabulary, and
|
|
62
|
+
* minting a park from here would give one ticket two transition authorities —
|
|
63
|
+
* the failure mode R14 exists to prevent.
|
|
64
|
+
*
|
|
65
|
+
* So what this does is the honest half: it stops the dispatch (the caller
|
|
66
|
+
* `continue`s), and it makes the expiry and its cause visible in the tick's
|
|
67
|
+
* warnings so an operator watching this side is not left wondering why a ticket
|
|
68
|
+
* sat still. The durable park is observed, not raced for.
|
|
69
|
+
*/
|
|
70
|
+
function reportShadowDeadlineExpired(deps, result, ticketKey, verdict, blockedAdvanceReason) {
|
|
71
|
+
const reason = blockedAdvanceReason
|
|
72
|
+
? `${SHADOW_STALE_DEADLINE_REASON}:${blockedAdvanceReason}`
|
|
73
|
+
: SHADOW_STALE_DEADLINE_REASON;
|
|
74
|
+
result.warnings.push(`shadow-freshness deadline elapsed for ${ticketKey}: ${reason}`);
|
|
75
|
+
deps.log(`[epic-reconcile] ${ticketKey} shadow watermark ${verdict} past deadline (${reason}); ` +
|
|
76
|
+
`holding dispatch — the server reconciler owns the needs_human park`);
|
|
77
|
+
}
|
|
40
78
|
/**
|
|
41
79
|
* Execute the deterministic observed→desired reconciliation pass. All I/O is
|
|
42
80
|
* behind injected seams; the ready-set is computed by pure code (no LLM).
|
|
@@ -158,32 +196,47 @@ export async function reconcileEpic(access, observed, plan, deps, supervisorConf
|
|
|
158
196
|
// not claim a dispatch key until the backend proves the run's shadow index
|
|
159
197
|
// covers every declared predecessor's merge watermark. Dependency-free nodes
|
|
160
198
|
// bypass the gate entirely so independent tickets keep their parallel behavior.
|
|
199
|
+
// BAPI-845: the EXPLICIT non-epic bypass. A ticket that declares no
|
|
200
|
+
// predecessors is not a member of an active epic/scope workflow, so it is
|
|
201
|
+
// exempted here — before any scope-status lookup and before any ticket-status
|
|
202
|
+
// mutation. Stating it rather than letting it fall out of a compound
|
|
203
|
+
// condition is what makes "a scope problem never blocks a non-epic ticket" a
|
|
204
|
+
// property this file asserts instead of one it happens to have: a stale,
|
|
205
|
+
// blocked, or failed scope cannot reach an independent ticket's dispatch at
|
|
206
|
+
// all, because the scope is never consulted for one.
|
|
161
207
|
const declaredDeps = plan.tickets.find((t) => t.ticket_key === ticketKey)?.depends_on ?? [];
|
|
162
|
-
|
|
163
|
-
|
|
208
|
+
const participatesInScopeWorkflow = declaredDeps.length > 0;
|
|
209
|
+
if (participatesInScopeWorkflow && deps.checkShadowFreshness) {
|
|
210
|
+
let decision;
|
|
164
211
|
try {
|
|
165
|
-
|
|
212
|
+
decision = normalizeShadowDecision(await deps.checkShadowFreshness(ticketKey));
|
|
166
213
|
}
|
|
167
214
|
catch (err) {
|
|
168
215
|
// Fail closed: a freshness API error / malformed response never opens the
|
|
169
|
-
// gate
|
|
170
|
-
|
|
216
|
+
// gate, and never parks either — an unread deadline is not an elapsed one.
|
|
217
|
+
// Hold this tick without claiming a key or consuming any counter.
|
|
218
|
+
decision = { verdict: "stale" };
|
|
171
219
|
deps.log(`[epic-reconcile] shadow-freshness error for ${ticketKey}: ${safeDiagnosticMessage(err, "freshness error")}; holding`);
|
|
172
220
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
//
|
|
185
|
-
|
|
186
|
-
|
|
221
|
+
const { verdict } = decision;
|
|
222
|
+
if (verdict === "stale" || verdict === "failed") {
|
|
223
|
+
// BAPI-678: `stale` covers ordinary unproven freshness AND the backend's
|
|
224
|
+
// warn-only downgrade of a `failed` shadow lifecycle; `failed` is the
|
|
225
|
+
// terminal one the backend has already blocked the run for. Both hold.
|
|
226
|
+
//
|
|
227
|
+
// BAPI-845 bounds that hold. While the backend reports the typed
|
|
228
|
+
// `shadow.stale_deadline_seconds` as unexpired the ticket simply waits —
|
|
229
|
+
// it is never dispatched and never routed to the canonical index, so a
|
|
230
|
+
// held ticket cannot quietly research against an index that does not
|
|
231
|
+
// contain its predecessors' merges. A ticket already in `parsing` is left
|
|
232
|
+
// exactly where it is; nothing on this path advances it.
|
|
233
|
+
const blocked = decision.blockedAdvanceReason ?? null;
|
|
234
|
+
const because = blocked ? ` (${blocked})` : "";
|
|
235
|
+
if (decision.deadlineExpired !== true) {
|
|
236
|
+
deps.log(`[epic-reconcile] holding ${ticketKey}: shadow watermark ${verdict}${because}`);
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
reportShadowDeadlineExpired(deps, result, ticketKey, verdict, blocked);
|
|
187
240
|
continue;
|
|
188
241
|
}
|
|
189
242
|
// "covered" | "not_applicable" fall through to the normal claim/dispatch.
|
|
@@ -27,7 +27,7 @@ import { makeLocalMergeExecutor, resolveLocalMergeMethod } from "./local-merge.j
|
|
|
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";
|
|
30
|
-
import { reconcileEpic } from "./epic-reconcile.js";
|
|
30
|
+
import { reconcileEpic, } from "./epic-reconcile.js";
|
|
31
31
|
import { normalizeDeclaredTouchedFiles } from "./file-scope-guard.js";
|
|
32
32
|
import { buildSupervisorRemediationWorkerMessage } from "./supervisor-message-relay.js";
|
|
33
33
|
import { sendWorkerMessage } from "./store.js";
|
|
@@ -1953,8 +1953,20 @@ export async function buildProductionEpicRuntimeDeps(epicKey) {
|
|
|
1953
1953
|
// BAPI-654 (T2): backend-authoritative shadow ops. Like fetchPlan, they
|
|
1954
1954
|
// receive `access` as a parameter, so they close over no credentials.
|
|
1955
1955
|
reconcileShadowMerge: (acc, ek, mergedTicketKey) => reconcileShadowMerge(acc, { epicKey: ek, mergedTicketKey }),
|
|
1956
|
-
|
|
1957
|
-
|
|
1956
|
+
// BAPI-845: forward the WHOLE decision, not just the verdict. Dropping the
|
|
1957
|
+
// deadline state here is what left the reconcile pass able only to hold
|
|
1958
|
+
// forever; the reason token is what lets it say why.
|
|
1959
|
+
checkShadowFreshness: async (acc, ek, tk) => {
|
|
1960
|
+
const freshness = await fetchShadowDispatchFreshness(acc, {
|
|
1961
|
+
epicKey: ek,
|
|
1962
|
+
ticketKey: tk,
|
|
1963
|
+
});
|
|
1964
|
+
return {
|
|
1965
|
+
verdict: freshness.verdict,
|
|
1966
|
+
deadlineExpired: freshness.deadlineExpired,
|
|
1967
|
+
blockedAdvanceReason: freshness.blockedAdvanceReason,
|
|
1968
|
+
};
|
|
1969
|
+
},
|
|
1958
1970
|
// BAPI-442 seams are wired at the reconcileDeps level inside runEpicTick
|
|
1959
1971
|
// (they need the per-tick `access` and `prBindings` closure). The factory
|
|
1960
1972
|
// returns the dispatchSeam with isReReview support; the other two seams are
|
|
@@ -100,6 +100,37 @@ export class ConductorLedgerSubprocessRuntimeError extends Error {
|
|
|
100
100
|
this.envVar = envVar;
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
|
+
/** Plain-session tools that answer the same questions without worker context. */
|
|
104
|
+
const WORKER_CONTEXT_ALTERNATIVES = "get_epic_snapshot, poll_events";
|
|
105
|
+
/**
|
|
106
|
+
* Raised (BAPI-772) when a worker-SCOPED operation is called from a plain MCP
|
|
107
|
+
* session — one that was never spawned by the conductor, so the worker
|
|
108
|
+
* environment is ABSENT rather than broken.
|
|
109
|
+
*
|
|
110
|
+
* This is a guidance envelope, not a fault: `check_messages` and the
|
|
111
|
+
* `wait_for_done_gate` emit leg used to surface an opaque validation error or a
|
|
112
|
+
* 503 in an ordinary session, leaving the caller to guess that the tool is
|
|
113
|
+
* worker-scoped at all. The message states where the context comes from and what
|
|
114
|
+
* to call instead, and is explicit that NO functional fallback was performed.
|
|
115
|
+
*
|
|
116
|
+
* Absence only. Worker env that is present but empty, relative, inaccessible, or
|
|
117
|
+
* otherwise invalid keeps the fail-loud
|
|
118
|
+
* {@link ConductorLedgerSubprocessRuntimeError} path — a corrupted runtime must
|
|
119
|
+
* never be relabeled as "you are just in a normal session".
|
|
120
|
+
*/
|
|
121
|
+
export class ConductorWorkerContextRequiredError extends Error {
|
|
122
|
+
missingEnvVars;
|
|
123
|
+
constructor(missingEnvVars) {
|
|
124
|
+
// Single-line, path-free, secret-free (the committed CLI-stderr invariant in
|
|
125
|
+
// security-regressions.test.ts requires it). Names only fixed env-var tokens.
|
|
126
|
+
super("This operation is available only inside a conductor-spawned worker session " +
|
|
127
|
+
"(created by `start-tickets --conductor` or an executor job). Missing worker " +
|
|
128
|
+
`context: ${missingEnvVars.join(", ")}. No fallback was performed. In a plain ` +
|
|
129
|
+
`session use ${WORKER_CONTEXT_ALTERNATIVES} instead.`);
|
|
130
|
+
this.name = "ConductorWorkerContextRequiredError";
|
|
131
|
+
this.missingEnvVars = missingEnvVars;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
103
134
|
/**
|
|
104
135
|
* Structural guard for a native-module-load failure raised by the conductor
|
|
105
136
|
* store (BAPI-526). Detected structurally (by `name` + a valid `details` object)
|
|
@@ -225,6 +256,22 @@ export function toConductorErrorEnvelope(error) {
|
|
|
225
256
|
details: { env_var: error.envVar, reason: error.reason },
|
|
226
257
|
};
|
|
227
258
|
}
|
|
259
|
+
// Worker-context guidance (BAPI-772): the caller is in a plain MCP session and
|
|
260
|
+
// the worker environment is ABSENT. A 400 (the module's context-required
|
|
261
|
+
// convention, shared with EPIC_TICK_V1_FROZEN) rather than a 503: nothing is
|
|
262
|
+
// broken and nothing becomes available by retrying. `details` carries only the
|
|
263
|
+
// fixed env-var NAMES — never their values, a path, argv, stderr, or a stack.
|
|
264
|
+
if (error instanceof ConductorWorkerContextRequiredError) {
|
|
265
|
+
return {
|
|
266
|
+
error: "WORKER_CONTEXT_REQUIRED",
|
|
267
|
+
status: 400,
|
|
268
|
+
message: error.message,
|
|
269
|
+
details: {
|
|
270
|
+
missing_env_vars: error.missingEnvVars.join(","),
|
|
271
|
+
alternatives: WORKER_CONTEXT_ALTERNATIVES,
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
}
|
|
228
275
|
if (error instanceof ConductorValidationError) {
|
|
229
276
|
// Validation messages are conductor-authored and name the offending field,
|
|
230
277
|
// never its value — but redact defensively so an adversarial/secret-bearing
|