@bridge_gpt/mcp-server 0.2.39 → 0.2.42
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 +10 -10
- package/build/agent-capabilities/cli.js +2 -1
- package/build/agent-launchers/claude-executor-adapter.js +17 -4
- package/build/claude-user-config-doctor.js +42 -11
- package/build/cli-release.js +2 -1
- package/build/commands.generated.js +4 -4
- package/build/conduct-epic/bridge-client.js +354 -113
- package/build/conduct-epic/checkpoint-store.js +75 -2
- package/build/conduct-epic/cli.js +795 -109
- package/build/conduct-epic/cut-protocol.js +327 -0
- package/build/conduct-epic/pr-state.js +113 -24
- package/build/conduct-epic/spawn.js +14 -2
- package/build/conductor/bridge-api-client.js +27 -1
- package/build/conductor/cli.js +46 -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/tools.js +32 -3
- package/build/conductor/worker-ledger-cli.js +27 -1
- package/build/conductor-bin.js +15 -15
- package/build/credentials-cli.js +3 -2
- package/build/doctor.js +107 -41
- package/build/executor/cli.js +48 -1
- package/build/executor/env.js +21 -0
- package/build/executor/index-scope.js +39 -0
- package/build/executor/job-log-registry.js +69 -0
- package/build/executor/job-runner.js +148 -26
- package/build/executor/live-worker-registry.js +83 -0
- package/build/executor/observation.js +167 -6
- package/build/executor/platform.js +147 -3
- package/build/executor/process.js +58 -14
- package/build/executor/runner.js +235 -48
- package/build/executor/test-clock.js +3 -2
- package/build/index-scope-contract.js +96 -0
- package/build/index.js +153 -204
- package/build/init.js +83 -22
- package/build/install-bridge-conductor.js +323 -14
- package/build/install-bridge.js +202 -38
- 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 +3 -3
- package/build/plane/defaults.js +4 -1
- package/build/plane/preflight.js +81 -10
- 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 +74 -1
- package/build/schedule-run.js +3 -2
- package/build/setup-epic.js +453 -78
- 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 +1 -1
- package/docs/CONDUCTOR.md +22 -0
- package/docs/install/mcp-tool-integrations.md +19 -3
- package/package.json +2 -2
package/build/credentials-cli.js
CHANGED
|
@@ -25,6 +25,7 @@ import { readFile, mkdir, writeFile, rename, chmod, unlink, open } from "fs/prom
|
|
|
25
25
|
import os from "os";
|
|
26
26
|
import readline from "readline";
|
|
27
27
|
import { migrateAgentConfigCredentialToStore, } from "./agent-config-credential-migration.js";
|
|
28
|
+
import { MCP_PACKAGE_NAME } from "./mcp-identity.js";
|
|
28
29
|
/** The only agent-config sources the migration knows how to scan. */
|
|
29
30
|
const ALLOWED_SOURCES = [".mcp.json", ".cursor/mcp.json"];
|
|
30
31
|
/** Every subcommand this CLI accepts, in help order. */
|
|
@@ -33,7 +34,7 @@ const SUBCOMMANDS = ["migrate-agent-config"];
|
|
|
33
34
|
export function getCredentialsUsage() {
|
|
34
35
|
return [
|
|
35
36
|
"Usage:",
|
|
36
|
-
|
|
37
|
+
` npx -y ${MCP_PACKAGE_NAME} credentials migrate-agent-config \\`,
|
|
37
38
|
" [--write-credentials|--no-write-credentials] \\",
|
|
38
39
|
" [--source=.mcp.json|--source=.cursor/mcp.json]",
|
|
39
40
|
"",
|
|
@@ -222,7 +223,7 @@ export async function runCredentialsCli(argv, overrides) {
|
|
|
222
223
|
deps.log(result.message);
|
|
223
224
|
deps.log("");
|
|
224
225
|
deps.log("To migrate it, re-run with --write-credentials:");
|
|
225
|
-
deps.log(
|
|
226
|
+
deps.log(` npx -y ${MCP_PACKAGE_NAME} credentials migrate-agent-config --write-credentials`);
|
|
226
227
|
if (result.candidates && result.candidates.length > 0) {
|
|
227
228
|
deps.log("");
|
|
228
229
|
deps.log("Discovered source(s):");
|
package/build/doctor.js
CHANGED
|
@@ -28,11 +28,11 @@ import { createBridgeApiUrls } from "./bridge-api-urls.js";
|
|
|
28
28
|
import { probeToolSurface } from "./tool-surface-gating.js";
|
|
29
29
|
import { resolveBapiCredentials } from "./credential-store.js";
|
|
30
30
|
import { resolveConductorBridgeApiAccess } from "./conductor/bridge-api-client.js";
|
|
31
|
-
import { getIndexBranch } from "./conduct-epic/bridge-client.js";
|
|
32
31
|
import { resolveConductEpicStateDirectory } from "./conduct-epic/checkpoint-store.js";
|
|
33
32
|
import { isConductEpicLockOwnerAlive, parseConductEpicLock, } from "./conduct-epic/lock.js";
|
|
34
33
|
import { resolveRequiredStartTicketsRepoName } from "./start-tickets-repo.js";
|
|
35
|
-
import { BRIDGE_PACKAGE_NAME, describeLauncherReason, inspectLauncherConfigs, parseLauncherPin as parseSharedLauncherPin, } from "./launcher-config-inspection.js";
|
|
34
|
+
import { BRIDGE_PACKAGE_NAME, describeLauncherReason, DUPLICATE_REGISTRATION_GUIDANCE, inspectLauncherConfigs, parseLauncherPin as parseSharedLauncherPin, } from "./launcher-config-inspection.js";
|
|
35
|
+
import { MCP_SERVER_NAME, MCP_PACKAGE_NAME, } from "./mcp-identity.js";
|
|
36
36
|
import { DEFAULT_AGENT_NAME, resolveAgentSpec, isAgentName, formatValidAgentNames, } from "./agent-registry.js";
|
|
37
37
|
import { getDoctorPrereqDescriptors, probePrerequisite, resolveWorktrunkBinary, } from "./start-tickets-prereqs.js";
|
|
38
38
|
import { resolveProfiles } from "./mcp-profile.js";
|
|
@@ -50,7 +50,7 @@ export const DOCTOR_REPORT_TITLE = "bridge doctor — read-only diagnostics";
|
|
|
50
50
|
export function getDoctorUsage() {
|
|
51
51
|
return [
|
|
52
52
|
"Usage:",
|
|
53
|
-
|
|
53
|
+
` npx -y ${MCP_PACKAGE_NAME} doctor [--agent <name>]`,
|
|
54
54
|
"",
|
|
55
55
|
// BAPI-669 (U9b): `doctor` is the general Bridge diagnostic command, not a
|
|
56
56
|
// start-tickets-only one — and its report leads with install status, so the
|
|
@@ -72,7 +72,7 @@ export function getDoctorUsage() {
|
|
|
72
72
|
"the start-tickets preflight prerequisites plus",
|
|
73
73
|
"uv, the selected agent's command, Bridge API credential resolution, and",
|
|
74
74
|
"worktree MCP registration reachability, and a read-only check of whether the",
|
|
75
|
-
|
|
75
|
+
`Claude user config (~/.claude.json) registers a '${MCP_SERVER_NAME}' MCP server that`,
|
|
76
76
|
"could shadow the registration provisioned into a worker worktree. That check",
|
|
77
77
|
"only reads the file and runs `git worktree list --porcelain`; it never edits,",
|
|
78
78
|
"migrates, or writes any configuration. Credential resolution reports the",
|
|
@@ -428,7 +428,14 @@ export async function inspectLauncherCache(deps) {
|
|
|
428
428
|
// but it is still a finding — doctor cannot determine its pin, and saying
|
|
429
429
|
// nothing would read as "this config is fine". Classify it before the
|
|
430
430
|
// no-entry skip below.
|
|
431
|
-
|
|
431
|
+
//
|
|
432
|
+
// `duplicate-registration` is included (BAPI-807) because its pin is
|
|
433
|
+
// genuinely indeterminate: there are two entries and doctor must not pick
|
|
434
|
+
// one. Omitting it here would fall through to the probe below with a null
|
|
435
|
+
// spec — the registration-status section reports the actual fault.
|
|
436
|
+
if (found.action === "invalid" ||
|
|
437
|
+
found.action === "unsupported" ||
|
|
438
|
+
found.action === "duplicate-registration") {
|
|
432
439
|
inspections.push({
|
|
433
440
|
relPath,
|
|
434
441
|
spec,
|
|
@@ -494,11 +501,79 @@ export async function inspectLauncherCache(deps) {
|
|
|
494
501
|
}
|
|
495
502
|
return inspections;
|
|
496
503
|
}
|
|
504
|
+
/** The exact INFO body every legacy-registration finding renders (pinned text). */
|
|
505
|
+
export const LEGACY_REGISTRATION_INFO_TEXT = `This registration remains supported. New installs use \`${MCP_SERVER_NAME}\`. No action is required.`;
|
|
506
|
+
/**
|
|
507
|
+
* Select registration-status findings from shared launcher inspections.
|
|
508
|
+
*
|
|
509
|
+
* Pure: it derives from inspections that were already computed read-only, and
|
|
510
|
+
* performs no I/O of its own.
|
|
511
|
+
*/
|
|
512
|
+
export function collectRegistrationStatusFindings(inspections) {
|
|
513
|
+
const findings = [];
|
|
514
|
+
for (const found of inspections) {
|
|
515
|
+
if (!found.filePresent)
|
|
516
|
+
continue;
|
|
517
|
+
if (found.action === "duplicate-registration") {
|
|
518
|
+
findings.push({ relPath: found.relPath, kind: "duplicate" });
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
if (found.legacyRegistration && found.registrationKey) {
|
|
522
|
+
findings.push({
|
|
523
|
+
relPath: found.relPath,
|
|
524
|
+
kind: "legacy",
|
|
525
|
+
registrationKey: found.registrationKey,
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return findings;
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* True when any finding is a duplicate registration — the ONE registration-status
|
|
533
|
+
* state that affects doctor's exit code.
|
|
534
|
+
*/
|
|
535
|
+
export function hasDuplicateRegistrationFault(findings) {
|
|
536
|
+
return findings.some((f) => f.kind === "duplicate");
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Collect registration-status findings read-only, reusing the shared launcher
|
|
540
|
+
* inspection. Takes the same injected deps as the cache probe minus the npx
|
|
541
|
+
* probe — this section spawns nothing at all.
|
|
542
|
+
*/
|
|
543
|
+
export async function inspectRegistrationStatus(deps) {
|
|
544
|
+
const shared = await inspectLauncherConfigs({
|
|
545
|
+
cwd: deps.cwd,
|
|
546
|
+
targetVersion: VERSION,
|
|
547
|
+
readFile: deps.readFile,
|
|
548
|
+
});
|
|
549
|
+
return collectRegistrationStatusFindings(shared);
|
|
550
|
+
}
|
|
551
|
+
/** Render the registration-status section of the doctor report (pure formatting). */
|
|
552
|
+
export function formatRegistrationStatusReport(findings) {
|
|
553
|
+
const lines = ["", "MCP registration status", ""];
|
|
554
|
+
if (findings.length === 0) {
|
|
555
|
+
lines.push(`OK Every inspected config registers Bridge as \`${MCP_SERVER_NAME}\`.`);
|
|
556
|
+
return lines.join("\n");
|
|
557
|
+
}
|
|
558
|
+
for (const finding of findings) {
|
|
559
|
+
if (finding.kind === "legacy") {
|
|
560
|
+
// Semantic status FIRST, then the affected path and the exact key, with
|
|
561
|
+
// code-like values in backticks.
|
|
562
|
+
lines.push(`INFO Supported legacy registration — \`${finding.relPath}\` registers Bridge as ` +
|
|
563
|
+
`\`${finding.registrationKey}\`.`);
|
|
564
|
+
lines.push(` ${LEGACY_REGISTRATION_INFO_TEXT}`);
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
lines.push(`ERROR Duplicate Bridge registrations detected — \`${finding.relPath}\`.`);
|
|
568
|
+
lines.push(` ${DUPLICATE_REGISTRATION_GUIDANCE}`);
|
|
569
|
+
}
|
|
570
|
+
return lines.join("\n");
|
|
571
|
+
}
|
|
497
572
|
/** Render the launcher-cache section of the doctor report (pure formatting). */
|
|
498
573
|
export function formatLauncherCacheReport(inspections) {
|
|
499
574
|
const lines = ["", "Launcher cache (MCP cold-start readiness)", ""];
|
|
500
575
|
if (inspections.length === 0) {
|
|
501
|
-
lines.push(
|
|
576
|
+
lines.push(`No project-local ${MCP_SERVER_NAME} launcher configs found to inspect.`);
|
|
502
577
|
return lines.join("\n");
|
|
503
578
|
}
|
|
504
579
|
const labels = {
|
|
@@ -645,7 +720,7 @@ export function formatLocalCliOverrideDiagnosticReport(diagnostic) {
|
|
|
645
720
|
const lines = ["", `Local CLI launcher (${LOCAL_CLI_OVERRIDE_ENV})`, ""];
|
|
646
721
|
switch (diagnostic.state) {
|
|
647
722
|
case "not-configured":
|
|
648
|
-
lines.push(
|
|
723
|
+
lines.push(`NOT CONFIGURED packaged launcher in use (npx -y ${MCP_PACKAGE_NAME})`);
|
|
649
724
|
lines.push(" No local override is set, so no launcher probe was run.");
|
|
650
725
|
break;
|
|
651
726
|
case "executable":
|
|
@@ -1062,7 +1137,6 @@ export async function collectConductEpicDiagnostic(deps) {
|
|
|
1062
1137
|
const resolveAccess = deps.resolveAccess ?? resolveConductorBridgeApiAccess;
|
|
1063
1138
|
let repo = null;
|
|
1064
1139
|
let bridgeCredentialResolved = false;
|
|
1065
|
-
let accessForIndexProbe = null;
|
|
1066
1140
|
try {
|
|
1067
1141
|
const result = await resolveAccess({
|
|
1068
1142
|
env: deps.env,
|
|
@@ -1075,7 +1149,6 @@ export async function collectConductEpicDiagnostic(deps) {
|
|
|
1075
1149
|
if (result.ok) {
|
|
1076
1150
|
repo = result.access.repoName;
|
|
1077
1151
|
bridgeCredentialResolved = true;
|
|
1078
|
-
accessForIndexProbe = result;
|
|
1079
1152
|
}
|
|
1080
1153
|
}
|
|
1081
1154
|
catch {
|
|
@@ -1124,21 +1197,6 @@ export async function collectConductEpicDiagnostic(deps) {
|
|
|
1124
1197
|
locks.push({ epic, owner_pid: owner.owner_pid, host: owner.host, alive });
|
|
1125
1198
|
}
|
|
1126
1199
|
}
|
|
1127
|
-
let indexOverrideBranch = null;
|
|
1128
|
-
let indexOverrideChecked = false;
|
|
1129
|
-
if (accessForIndexProbe !== null && accessForIndexProbe.ok) {
|
|
1130
|
-
const probe = deps.getIndexBranch ?? getIndexBranch;
|
|
1131
|
-
try {
|
|
1132
|
-
const result = await probe(accessForIndexProbe.access);
|
|
1133
|
-
if (result.ok) {
|
|
1134
|
-
indexOverrideChecked = true;
|
|
1135
|
-
indexOverrideBranch = result.value.override?.override_branch ?? null;
|
|
1136
|
-
}
|
|
1137
|
-
}
|
|
1138
|
-
catch {
|
|
1139
|
-
indexOverrideChecked = false;
|
|
1140
|
-
}
|
|
1141
|
-
}
|
|
1142
1200
|
return {
|
|
1143
1201
|
ghPresent,
|
|
1144
1202
|
ghAuthenticated,
|
|
@@ -1150,8 +1208,6 @@ export async function collectConductEpicDiagnostic(deps) {
|
|
|
1150
1208
|
stateDirectoryReadable,
|
|
1151
1209
|
checkpoints,
|
|
1152
1210
|
locks,
|
|
1153
|
-
indexOverrideBranch,
|
|
1154
|
-
indexOverrideChecked,
|
|
1155
1211
|
};
|
|
1156
1212
|
}
|
|
1157
1213
|
/** Render the conduct-epic section. Advisory: never changes the exit code. */
|
|
@@ -1206,18 +1262,8 @@ export function formatConductEpicDiagnosticReport(diagnostic) {
|
|
|
1206
1262
|
}
|
|
1207
1263
|
}
|
|
1208
1264
|
}
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
}
|
|
1212
|
-
else if (diagnostic.indexOverrideBranch === null) {
|
|
1213
|
-
lines.push(" OK Indexed-branch override: none active.");
|
|
1214
|
-
}
|
|
1215
|
-
else {
|
|
1216
|
-
lines.push(` WARN Indexed-branch override: active on '${diagnostic.indexOverrideBranch}'.\n` +
|
|
1217
|
-
" If no epic is running, this is stale — run `conduct-epic finish <EPIC>`.");
|
|
1218
|
-
}
|
|
1219
|
-
lines.push(" Read-only: this section probes commands, reads local state, and issues one");
|
|
1220
|
-
lines.push(" GET. It recovers no lock, rewrites no checkpoint, and never changes the exit code.");
|
|
1265
|
+
lines.push(" Read-only: this section probes commands and reads local state. It recovers no");
|
|
1266
|
+
lines.push(" lock, rewrites no checkpoint, and never changes the exit code.");
|
|
1221
1267
|
return lines.join("\n");
|
|
1222
1268
|
}
|
|
1223
1269
|
/**
|
|
@@ -1314,6 +1360,25 @@ export async function runDoctorCli(argv, overrides = {}) {
|
|
|
1314
1360
|
/* adapter diagnostics are advisory; never block the doctor report */
|
|
1315
1361
|
}
|
|
1316
1362
|
}
|
|
1363
|
+
// Registration-status diagnostics (BAPI-807). Strictly read-only and spawns
|
|
1364
|
+
// nothing. UNLIKE every other advisory section, one of its outcomes CAN change
|
|
1365
|
+
// the exit code: a duplicate registration is an actionable fault that leaves
|
|
1366
|
+
// Bridge unable to reconcile the config at all, so `doctor` must not exit 0 on
|
|
1367
|
+
// it. A supported legacy registration is information only and never does.
|
|
1368
|
+
let duplicateRegistrationFault = false;
|
|
1369
|
+
try {
|
|
1370
|
+
const registrationFindings = await inspectRegistrationStatus({
|
|
1371
|
+
cwd: overrides.launcherProbe?.cwd ?? deps.cwd,
|
|
1372
|
+
readFile: overrides.launcherProbe?.readFile ?? ((p) => readFile(p, "utf-8")),
|
|
1373
|
+
});
|
|
1374
|
+
duplicateRegistrationFault = hasDuplicateRegistrationFault(registrationFindings);
|
|
1375
|
+
log(formatRegistrationStatusReport(registrationFindings));
|
|
1376
|
+
}
|
|
1377
|
+
catch {
|
|
1378
|
+
// An unexpected failure must not invent a fault, and must not drop the
|
|
1379
|
+
// section silently either — but it also cannot claim the configs are clean.
|
|
1380
|
+
log(formatRegistrationStatusReport([]));
|
|
1381
|
+
}
|
|
1317
1382
|
// Strictly read-only launcher-cache diagnostics (BAPI-451). Best-effort: a probe
|
|
1318
1383
|
// failure never changes the doctor exit code (cold-start readiness is advisory,
|
|
1319
1384
|
// not a hard prerequisite). The exit code remains driven by required prereqs.
|
|
@@ -1480,7 +1545,6 @@ export async function runDoctorCli(argv, overrides = {}) {
|
|
|
1480
1545
|
readdir: overrides.conductEpic?.readdir ?? ((p) => readdir(p)),
|
|
1481
1546
|
runCommand: overrides.conductEpic?.runCommand ?? deps.runCommand,
|
|
1482
1547
|
resolveAccess: overrides.conductEpic?.resolveAccess,
|
|
1483
|
-
getIndexBranch: overrides.conductEpic?.getIndexBranch,
|
|
1484
1548
|
isProcessAlive: overrides.conductEpic?.isProcessAlive,
|
|
1485
1549
|
};
|
|
1486
1550
|
log(formatConductEpicDiagnosticReport(await collectConductEpicDiagnostic(conductDeps)));
|
|
@@ -1499,12 +1563,14 @@ export async function runDoctorCli(argv, overrides = {}) {
|
|
|
1499
1563
|
stateDirectoryReadable: false,
|
|
1500
1564
|
checkpoints: [],
|
|
1501
1565
|
locks: [],
|
|
1502
|
-
indexOverrideBranch: null,
|
|
1503
|
-
indexOverrideChecked: false,
|
|
1504
1566
|
}));
|
|
1505
1567
|
}
|
|
1506
1568
|
}
|
|
1507
1569
|
if (!collection.ok)
|
|
1508
1570
|
return 1;
|
|
1571
|
+
// BAPI-807: a duplicate registration fails the run alongside a missing
|
|
1572
|
+
// prerequisite. A supported legacy registration contributes nothing here.
|
|
1573
|
+
if (duplicateRegistrationFault)
|
|
1574
|
+
return 1;
|
|
1509
1575
|
return collection.results.some((r) => !r.found) ? 1 : 0;
|
|
1510
1576
|
}
|
package/build/executor/cli.js
CHANGED
|
@@ -14,6 +14,7 @@ import { DEFAULT_EXECUTOR_AGENT_ID } from "./agent-identity.js";
|
|
|
14
14
|
import { createDefaultExecutorDeps } from "./deps.js";
|
|
15
15
|
import { resolveBaseUrl, resolveExecutorApiAccess, EXECUTOR_BASE_URL_REQUIRED_MESSAGE, } from "./credentials.js";
|
|
16
16
|
import { createExecutorHttpClient } from "./http-client.js";
|
|
17
|
+
import { startExecutorSleepAssertion } from "./platform.js";
|
|
17
18
|
import { runExecutor } from "./runner.js";
|
|
18
19
|
import { runExecutorWatchCli } from "./watch-cli.js";
|
|
19
20
|
/** Fixed executor timing/behavior defaults. */
|
|
@@ -210,6 +211,21 @@ export function parseExecutorArgs(argv, context) {
|
|
|
210
211
|
};
|
|
211
212
|
return { kind: "ok", options };
|
|
212
213
|
}
|
|
214
|
+
/**
|
|
215
|
+
* Default signal registrar: `process.on`, disposed with `process.off`.
|
|
216
|
+
*
|
|
217
|
+
* The SAME register-and-return-an-unsubscriber shape the notifications CLI and
|
|
218
|
+
* the plane supervisor already use. Defined here rather than inside `runExecutor`
|
|
219
|
+
* so the runner never touches `process` itself — the runner stays a pure loop
|
|
220
|
+
* over injected boundaries, and this bootstrap file remains the one place that
|
|
221
|
+
* knows the process object exists.
|
|
222
|
+
*/
|
|
223
|
+
function defaultSignalRegistrar(signal, handler) {
|
|
224
|
+
process.on(signal, handler);
|
|
225
|
+
return () => {
|
|
226
|
+
process.off(signal, handler);
|
|
227
|
+
};
|
|
228
|
+
}
|
|
213
229
|
function hasRepoFlag(argv) {
|
|
214
230
|
return argv.some((a) => a === "--repo" || a === "--repos" || a.startsWith("--repos="));
|
|
215
231
|
}
|
|
@@ -306,12 +322,43 @@ export async function runExecutorCli(argv, overrides = {}) {
|
|
|
306
322
|
// Hand the loop the VALIDATED runtime configuration: the parsed options plus the
|
|
307
323
|
// normalized base URL resolution actually settled on.
|
|
308
324
|
const runtimeOptions = { ...options, resolvedBaseUrl };
|
|
325
|
+
// --- Sleep assertion (BAPI-828) ---------------------------------------
|
|
326
|
+
// Started HERE — after argument parsing, base-URL resolution, credential
|
|
327
|
+
// resolution, and HTTP client construction have all succeeded, and immediately
|
|
328
|
+
// before the loop begins. That position is the whole design: it guarantees the
|
|
329
|
+
// assertion exists before the first claim can spawn a worker, while a `--help`
|
|
330
|
+
// run, an unknown flag, a missing base URL, and a credential failure each
|
|
331
|
+
// return above without ever spawning `caffeinate`. Fail-open throughout — a
|
|
332
|
+
// host that cannot hold the assertion still claims and still works.
|
|
333
|
+
const sleepAssertion = startExecutorSleepAssertion({
|
|
334
|
+
platform: context.platform,
|
|
335
|
+
// The EXECUTOR's environment, read straight from deps. A worker environment
|
|
336
|
+
// never reaches this call, and the opt-out is never forwarded to one.
|
|
337
|
+
env: deps.env,
|
|
338
|
+
cwd: deps.cwd,
|
|
339
|
+
executorPid: context.pid,
|
|
340
|
+
spawnProcess: deps.spawnProcess,
|
|
341
|
+
setTimer: deps.setTimer,
|
|
342
|
+
clearTimer: deps.clearTimer,
|
|
343
|
+
errorLog,
|
|
344
|
+
});
|
|
345
|
+
const runnerSeams = {
|
|
346
|
+
onSignal: overrides.onSignal ?? defaultSignalRegistrar,
|
|
347
|
+
};
|
|
309
348
|
try {
|
|
310
|
-
return await run(runtimeOptions, deps, httpClient);
|
|
349
|
+
return await run(runtimeOptions, deps, httpClient, runnerSeams);
|
|
311
350
|
}
|
|
312
351
|
catch (err) {
|
|
313
352
|
const message = err instanceof Error ? err.message : String(err);
|
|
314
353
|
errorLog(`Error: executor exited unexpectedly: ${message.slice(0, 200)}`);
|
|
315
354
|
return 1;
|
|
316
355
|
}
|
|
356
|
+
finally {
|
|
357
|
+
// Released for a normal completion, a graceful signal shutdown, AND a thrown
|
|
358
|
+
// runner error alike. Without the `finally` the throw path above would return
|
|
359
|
+
// its exit code while still holding a `caffeinate` child — which, being a
|
|
360
|
+
// live child of this process, is exactly the kind of thing that keeps a Node
|
|
361
|
+
// process from exiting when nobody calls `process.exit`.
|
|
362
|
+
await sleepAssertion.release();
|
|
363
|
+
}
|
|
317
364
|
}
|
package/build/executor/env.js
CHANGED
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
* (`Read(~/.config/bridge/**)` in `permissions.ts`).
|
|
41
41
|
*/
|
|
42
42
|
import { PR_BASE_BRANCH_ENV_VAR } from "../pr-base-contract.js";
|
|
43
|
+
import { INDEX_SCOPE_ENV_VAR, validateOptionalIndexScope } from "../index-scope-contract.js";
|
|
43
44
|
/** Non-secret operational keys forwarded to the worker when present. */
|
|
44
45
|
const ALLOWED_ENV_KEYS = [
|
|
45
46
|
"PATH",
|
|
@@ -92,6 +93,14 @@ export const EXPLICIT_DENY_KEYS = [
|
|
|
92
93
|
"GH_TOKEN",
|
|
93
94
|
"CLAUDE_CONFIG_DIR",
|
|
94
95
|
"XDG_CONFIG_HOME",
|
|
96
|
+
// BAPI-844: the index-scope declaration follows the BAPI_BASE_BRANCH rule —
|
|
97
|
+
// absent from ALLOWED_ENV_KEYS so it is never copied from `parentEnv`, and
|
|
98
|
+
// denied EXPLICITLY here so an operator's ambient `BAPI_INDEX_SCOPE` can never
|
|
99
|
+
// reach a worker. A worker's research index is a run-scoped decision; a value
|
|
100
|
+
// exported in the shell that happened to launch the executor is not that
|
|
101
|
+
// decision, and inheriting one would route an unscoped worker's plan and review
|
|
102
|
+
// at another epic's shadow index while nothing in the job said so.
|
|
103
|
+
INDEX_SCOPE_ENV_VAR,
|
|
95
104
|
];
|
|
96
105
|
/**
|
|
97
106
|
* True only when `key` is a safe, allowlisted operational key. Exported so the
|
|
@@ -130,10 +139,22 @@ export function buildExecutorBaseWorkerEnv(parentEnv, options = {}) {
|
|
|
130
139
|
}
|
|
131
140
|
}
|
|
132
141
|
env.BRIDGE_SKIP_PREPUSH = "1";
|
|
142
|
+
env.MAX_MCP_OUTPUT_TOKENS = "50000";
|
|
133
143
|
if (typeof options.effectiveBaseBranch === "string" &&
|
|
134
144
|
options.effectiveBaseBranch.length > 0) {
|
|
135
145
|
env[PR_BASE_BRANCH_ENV_VAR] = options.effectiveBaseBranch;
|
|
136
146
|
}
|
|
147
|
+
// BAPI-844: the index-scope declaration, set ONLY from the explicit option and
|
|
148
|
+
// only after the shared shape check. A malformed explicit value throws here —
|
|
149
|
+
// before an environment exists — rather than being dropped: silently omitting it
|
|
150
|
+
// would spawn a worker that researches the canonical index while the job it came
|
|
151
|
+
// from declared a scope, which is the one outcome the declaration exists to
|
|
152
|
+
// prevent. An absent option adds no key at all, so the returned object is
|
|
153
|
+
// unchanged for every unscoped worker.
|
|
154
|
+
const indexScope = validateOptionalIndexScope(options.indexScope);
|
|
155
|
+
if (indexScope !== undefined) {
|
|
156
|
+
env[INDEX_SCOPE_ENV_VAR] = indexScope;
|
|
157
|
+
}
|
|
137
158
|
// Nothing agent-specific is added here, and deliberately nothing secret. An
|
|
138
159
|
// adapter that needs to forward an operator-owned credential declares it as a
|
|
139
160
|
// passthrough and copies it onto the object this function returns; see
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-job index-scope resolution (BAPI-844).
|
|
3
|
+
*
|
|
4
|
+
* The server names the index scope a v2 job's worker must research against, as
|
|
5
|
+
* the top-level `index_scope_id` on the claimed job — the same wire position as
|
|
6
|
+
* `epic_run_id`, and for the same reason: it is a server-minted IDENTITY, not
|
|
7
|
+
* job-type-specific payload data. There is exactly one wire representation, and
|
|
8
|
+
* this module is the only place the executor reads it.
|
|
9
|
+
*
|
|
10
|
+
* Mirrors {@link ../executor/base-branch.ts} deliberately, including its posture:
|
|
11
|
+
*
|
|
12
|
+
* - ABSENT → an unscoped job. The worker's environment, command, and routing are
|
|
13
|
+
* byte-identical to the pre-BAPI-844 ones.
|
|
14
|
+
* - PRESENT and well-formed → carried into the worker environment as an explicit
|
|
15
|
+
* spawn option (never inherited).
|
|
16
|
+
* - PRESENT but malformed → a contract failure, resolved BEFORE any side effect.
|
|
17
|
+
* Dropping it instead would spawn a worker that silently researched the
|
|
18
|
+
* canonical index while the job it came from declared a scope.
|
|
19
|
+
*/
|
|
20
|
+
import { validateOptionalIndexScope } from "../index-scope-contract.js";
|
|
21
|
+
/**
|
|
22
|
+
* Resolve the declared index scope for one executor job.
|
|
23
|
+
*
|
|
24
|
+
* The diagnostic names the contract only. The submitted value never appears in
|
|
25
|
+
* it, because this string reaches `/fail`, the job row, and operator output —
|
|
26
|
+
* all places an opaque routing token must not be persisted.
|
|
27
|
+
*/
|
|
28
|
+
export function resolveExecutorJobIndexScope(job) {
|
|
29
|
+
try {
|
|
30
|
+
const indexScope = validateOptionalIndexScope(job.index_scope_id);
|
|
31
|
+
return indexScope === undefined ? { ok: true } : { ok: true, indexScope };
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return {
|
|
35
|
+
ok: false,
|
|
36
|
+
error: "job index_scope_id is present but is not a valid index-scope identity.",
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -82,6 +82,75 @@ export async function markExecutorJobLogFinished(jobId, finishedAt, deps) {
|
|
|
82
82
|
const filePath = jobLogRecordPath(stateDir, jobId, deps.platform ?? process.platform);
|
|
83
83
|
await deps.writeFile(filePath, JSON.stringify(normalizeRecord(updated), null, 2));
|
|
84
84
|
}
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
// Active worker logs (BAPI-828)
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
/**
|
|
89
|
+
* In-memory map of job id → worker-log path for jobs whose tee is currently open.
|
|
90
|
+
*
|
|
91
|
+
* DELIBERATELY SEPARATE FROM THE PERSISTED RECORD ABOVE, and deliberately not
|
|
92
|
+
* derived from it. The persisted record is a visibility nicety for a SEPARATE
|
|
93
|
+
* `executor watch` process, it is written with best-effort error swallowing, and
|
|
94
|
+
* it keeps its row after `finished_at` is stamped. This map answers a different
|
|
95
|
+
* question — "which worker logs can be appended to right now, in THIS process" —
|
|
96
|
+
* and the suspend diagnostic depends on that answer being correct. Deriving it
|
|
97
|
+
* from the persisted record would mean a failed registry write silently disables
|
|
98
|
+
* suspend annotation for a worker whose tee is perfectly healthy.
|
|
99
|
+
*
|
|
100
|
+
* Process-scoped module state rather than runner-scoped, unlike the live worker
|
|
101
|
+
* registry: entries are keyed by server-minted job id, so two concurrent
|
|
102
|
+
* `runExecutor` invocations in one process cannot collide, and each value is a
|
|
103
|
+
* path only its own job ever deactivates.
|
|
104
|
+
*
|
|
105
|
+
* SECRET-FREE: a filesystem path inside the job's own worktree, nothing else.
|
|
106
|
+
*/
|
|
107
|
+
const activeJobLogPaths = new Map();
|
|
108
|
+
/**
|
|
109
|
+
* Mark a job's worker log as open and appendable.
|
|
110
|
+
*
|
|
111
|
+
* Called once the tee EXISTS, before any registry persistence — see the map's
|
|
112
|
+
* docstring for why the two are not chained. Re-activating the same job id simply
|
|
113
|
+
* overwrites the path.
|
|
114
|
+
*/
|
|
115
|
+
export function activateJobLog(jobId, logPath) {
|
|
116
|
+
if (typeof logPath !== "string" || logPath.length === 0)
|
|
117
|
+
return;
|
|
118
|
+
activeJobLogPaths.set(jobId, logPath);
|
|
119
|
+
}
|
|
120
|
+
/** Stop appending to a job's worker log. Idempotent. */
|
|
121
|
+
export function deactivateJobLog(jobId) {
|
|
122
|
+
activeJobLogPaths.delete(jobId);
|
|
123
|
+
}
|
|
124
|
+
/** The distinct currently-appendable log paths (a snapshot, safe to iterate). */
|
|
125
|
+
export function activeJobLogPathsSnapshot() {
|
|
126
|
+
return [...new Set(activeJobLogPaths.values())];
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Append one diagnostic line to every currently active worker log.
|
|
130
|
+
*
|
|
131
|
+
* FAIL-OPEN PER PATH. Each append is contained independently, so one missing,
|
|
132
|
+
* deleted, or unwritable log cannot suppress the diagnostic for the others and
|
|
133
|
+
* cannot propagate an error into the executor's poll loop — this is annotation,
|
|
134
|
+
* never correctness.
|
|
135
|
+
*
|
|
136
|
+
* Paths are DEDUPLICATED (two jobs can legitimately name the same log during a
|
|
137
|
+
* re-entry window) and exactly one trailing newline is written, whether or not
|
|
138
|
+
* the caller supplied one.
|
|
139
|
+
*/
|
|
140
|
+
export async function appendToActiveJobLogs(line, deps) {
|
|
141
|
+
const paths = activeJobLogPathsSnapshot();
|
|
142
|
+
if (paths.length === 0)
|
|
143
|
+
return;
|
|
144
|
+
const payload = `${line.replace(/\n+$/, "")}\n`;
|
|
145
|
+
await Promise.all(paths.map(async (logPath) => {
|
|
146
|
+
try {
|
|
147
|
+
await deps.appendFile(logPath, payload);
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
/* one unwritable log never blocks the others, or the executor */
|
|
151
|
+
}
|
|
152
|
+
}));
|
|
153
|
+
}
|
|
85
154
|
/**
|
|
86
155
|
* Load and validate a record for `executor watch <job>`. Returns a structured
|
|
87
156
|
* missing/invalid result rather than throwing raw filesystem/JSON errors.
|