@bridge_gpt/mcp-server 0.2.57 → 0.2.59
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 +112 -113
- 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 +5 -1
- package/build/codex-launch-scope.js +13 -2
- package/build/commands.generated.js +5 -6
- package/build/conductor/bridge-api-client.js +149 -0
- package/build/conductor/bring-up-facts.js +1 -1
- package/build/conductor/claude-hook.js +16 -16
- package/build/conductor/doctor.js +24 -19
- package/build/conductor/epic-implementer-cli.js +38 -5
- package/build/conductor/epic-reconcile.js +1 -1
- package/build/conductor/epic-runtime.js +10 -10
- 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 +6 -6
- 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 +7 -7
- package/build/conductor/readiness.js +2 -2
- package/build/conductor/recovery-cli.js +243 -12
- package/build/conductor/recovery-operations.js +56 -4
- 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/cursor-agent-progress.js +72 -0
- package/build/doctor.js +20 -296
- package/build/drive-epic.js +65 -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 +155 -0
- package/build/epic-integration-pr.js +2 -2
- package/build/epic-shared/bridge-client.js +773 -0
- package/build/epic-shared/cut-protocol.js +392 -0
- package/build/executor/env.js +19 -8
- package/build/executor/heartbeat.js +1 -1
- package/build/executor/job-errors.js +3 -3
- package/build/executor/job-runner.js +1 -1
- package/build/executor/merge-job.js +35 -3
- 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/implement-epic/cli.js +1 -1
- package/build/index.js +187 -224
- package/build/init.js +20 -28
- package/build/install-bridge-conductor.js +7 -7
- package/build/install-bridge.js +203 -10
- package/build/install-doctor.js +1 -1
- package/build/mcp-host-entry-adapter.js +6 -0
- package/build/mcp-host-targets.js +2 -0
- 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 +15 -0
- package/build/model-routing-host.js +25 -0
- package/build/model-routing-preferences.js +99 -0
- package/build/pipelines.generated.js +11 -11
- package/build/plan-epic-implementer-eligibility.js +215 -0
- package/build/plan-execution-ledger.js +8 -0
- package/build/plane/member-roster.js +5 -5
- 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/setup-epic.js +8 -8
- package/build/start-tickets-conductor.js +32 -32
- package/build/start-tickets-prereqs.js +4 -9
- package/build/start-tickets.js +494 -98
- 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 +2 -2
- package/pipelines/implement-ticket.json +2 -2
- package/pipelines/plan-epic.json +2 -2
- package/pipelines/review-ticket.json +1 -1
package/build/doctor.js
CHANGED
|
@@ -22,6 +22,8 @@ import { createDefaultStartTicketsDeps } from "./start-tickets.js";
|
|
|
22
22
|
import { VERSION, BUILD_COMMIT } from "./version.generated.js";
|
|
23
23
|
import { collectInstallStatusChecks, formatInstallStatusReport, formatInstallStatusFallbackReport, resolveInstallDoctorTarget, } from "./install-doctor.js";
|
|
24
24
|
import { codexHome, inspectBridgeCodexProfile } from "./codex-bridge-profile.js";
|
|
25
|
+
import { resolveRoutingHost } from "./model-routing-host.js";
|
|
26
|
+
import { modelRoutingPreferencesPath, readModelRoutingPreference } from "./model-routing-preferences.js";
|
|
25
27
|
import { inspectExecutorAgentAdapter, inspectionForFailedResolution, } from "./agent-launchers/executor-adapter-inspection.js";
|
|
26
28
|
import { resolveExecutorAgentAdapter } from "./agent-launchers/executor-adapter-registry.js";
|
|
27
29
|
import { parseDefaultOnEnvFlag } from "./env-flags.js";
|
|
@@ -29,15 +31,13 @@ import { createBridgeApiUrls } from "./bridge-api-urls.js";
|
|
|
29
31
|
import { probeToolSurface } from "./tool-surface-gating.js";
|
|
30
32
|
import { resolveBapiCredentials } from "./credential-store.js";
|
|
31
33
|
import { resolveConductorBridgeApiAccess } from "./conductor/bridge-api-client.js";
|
|
32
|
-
import { getLiveRepositoryConductors, getParseDispatcherHealth, } from "./
|
|
33
|
-
import { resolveConductEpicStateDirectory } from "./implement-epic/checkpoint-store.js";
|
|
34
|
-
import { isConductEpicLockOwnerAlive, parseConductEpicLock, } from "./implement-epic/lock.js";
|
|
34
|
+
import { getLiveRepositoryConductors, getParseDispatcherHealth, } from "./epic-shared/bridge-client.js";
|
|
35
35
|
import { resolveRequiredStartTicketsRepoName } from "./start-tickets-repo.js";
|
|
36
36
|
import { collectWorkerContainmentDiagnostic, formatWorkerContainmentDiagnosticReport, } from "./worker-containment-diagnostic.js";
|
|
37
37
|
import { BRIDGE_PACKAGE_NAME, describeLauncherReason, DUPLICATE_REGISTRATION_GUIDANCE, inspectLauncherConfigs, parseLauncherPin as parseSharedLauncherPin, } from "./launcher-config-inspection.js";
|
|
38
38
|
import { MCP_SERVER_NAME, MCP_PACKAGE_NAME, } from "./mcp-identity.js";
|
|
39
39
|
import { DEFAULT_AGENT_NAME, resolveAgentSpec, isAgentName, formatValidAgentNames, } from "./agent-registry.js";
|
|
40
|
-
import { getDoctorPrereqDescriptors, probePrerequisite,
|
|
40
|
+
import { getDoctorPrereqDescriptors, probePrerequisite, } from "./start-tickets-prereqs.js";
|
|
41
41
|
import { resolveProfiles } from "./mcp-profile.js";
|
|
42
42
|
import { executorLaunchdDirForHome, executorSystemdDirForHome, executorIdFromLaunchdFilename, executorIdFromSystemdFilename, inspectExecutorServiceArtifact, EXECUTOR_SERVICE_BASE_URL_ENV, } from "./executor/index.js";
|
|
43
43
|
import { DEFAULT_BAPI_BASE_URL } from "./executor/credentials.js";
|
|
@@ -1311,175 +1311,6 @@ export function formatPlaneCredentialDiagnosticReport(diagnostic) {
|
|
|
1311
1311
|
lines.push(" no .bridge/plane/ directory, persists nothing, and starts no plane member.");
|
|
1312
1312
|
return lines.join("\n");
|
|
1313
1313
|
}
|
|
1314
|
-
/**
|
|
1315
|
-
* Collect implement-epic readiness, read-only.
|
|
1316
|
-
*
|
|
1317
|
-
* The credential is resolved only to establish that it CAN be, and the resolved
|
|
1318
|
-
* access object is discarded immediately: only the repo name and a boolean
|
|
1319
|
-
* survive into the returned diagnostic, so no later formatting change can print
|
|
1320
|
-
* a key it never received.
|
|
1321
|
-
*/
|
|
1322
|
-
export async function collectConductEpicDiagnostic(deps) {
|
|
1323
|
-
const isAlive = deps.isProcessAlive ?? isConductEpicLockOwnerAlive;
|
|
1324
|
-
let ghPresent = false;
|
|
1325
|
-
let ghAuthenticated = false;
|
|
1326
|
-
try {
|
|
1327
|
-
const version = await deps.runCommand("gh", ["--version"], { cwd: deps.cwd });
|
|
1328
|
-
ghPresent = version.exitCode === 0;
|
|
1329
|
-
}
|
|
1330
|
-
catch {
|
|
1331
|
-
ghPresent = false;
|
|
1332
|
-
}
|
|
1333
|
-
if (ghPresent) {
|
|
1334
|
-
try {
|
|
1335
|
-
const auth = await deps.runCommand("gh", ["auth", "status"], { cwd: deps.cwd });
|
|
1336
|
-
ghAuthenticated = auth.exitCode === 0;
|
|
1337
|
-
}
|
|
1338
|
-
catch {
|
|
1339
|
-
ghAuthenticated = false;
|
|
1340
|
-
}
|
|
1341
|
-
}
|
|
1342
|
-
const worktrunkBinary = resolveWorktrunkBinary(deps.platform, deps.env);
|
|
1343
|
-
let worktrunkAvailable = false;
|
|
1344
|
-
try {
|
|
1345
|
-
const probe = await deps.runCommand(worktrunkBinary, ["--version"], { cwd: deps.cwd });
|
|
1346
|
-
worktrunkAvailable = probe.exitCode === 0;
|
|
1347
|
-
}
|
|
1348
|
-
catch {
|
|
1349
|
-
worktrunkAvailable = false;
|
|
1350
|
-
}
|
|
1351
|
-
const resolveAccess = deps.resolveAccess ?? resolveConductorBridgeApiAccess;
|
|
1352
|
-
let repo = null;
|
|
1353
|
-
let bridgeCredentialResolved = false;
|
|
1354
|
-
try {
|
|
1355
|
-
const result = await resolveAccess({
|
|
1356
|
-
env: deps.env,
|
|
1357
|
-
cwd: deps.cwd,
|
|
1358
|
-
homedir: deps.homedir,
|
|
1359
|
-
platform: deps.platform,
|
|
1360
|
-
readFile: deps.readFile,
|
|
1361
|
-
stat: deps.stat,
|
|
1362
|
-
});
|
|
1363
|
-
if (result.ok) {
|
|
1364
|
-
repo = result.access.repoName;
|
|
1365
|
-
bridgeCredentialResolved = true;
|
|
1366
|
-
}
|
|
1367
|
-
}
|
|
1368
|
-
catch {
|
|
1369
|
-
bridgeCredentialResolved = false;
|
|
1370
|
-
}
|
|
1371
|
-
let stateDirectory = null;
|
|
1372
|
-
let stateDirectoryReadable = false;
|
|
1373
|
-
const checkpoints = [];
|
|
1374
|
-
const locks = [];
|
|
1375
|
-
if (repo !== null) {
|
|
1376
|
-
stateDirectory = resolveConductEpicStateDirectory(repo, {
|
|
1377
|
-
env: deps.env,
|
|
1378
|
-
homedir: deps.homedir,
|
|
1379
|
-
});
|
|
1380
|
-
let entries = [];
|
|
1381
|
-
try {
|
|
1382
|
-
entries = await deps.readdir(stateDirectory);
|
|
1383
|
-
stateDirectoryReadable = true;
|
|
1384
|
-
}
|
|
1385
|
-
catch {
|
|
1386
|
-
stateDirectoryReadable = false;
|
|
1387
|
-
}
|
|
1388
|
-
for (const entry of entries) {
|
|
1389
|
-
if (entry.endsWith(".json"))
|
|
1390
|
-
checkpoints.push(entry);
|
|
1391
|
-
if (!entry.endsWith(".lock"))
|
|
1392
|
-
continue;
|
|
1393
|
-
const epic = entry.slice(0, -".lock".length);
|
|
1394
|
-
let raw;
|
|
1395
|
-
try {
|
|
1396
|
-
raw = await deps.readFile(path.join(stateDirectory, entry));
|
|
1397
|
-
}
|
|
1398
|
-
catch {
|
|
1399
|
-
locks.push({ epic, owner_pid: null, host: null, alive: null, problem: "unreadable" });
|
|
1400
|
-
continue;
|
|
1401
|
-
}
|
|
1402
|
-
const owner = parseConductEpicLock(raw);
|
|
1403
|
-
if (owner === null) {
|
|
1404
|
-
locks.push({ epic, owner_pid: null, host: null, alive: null, problem: "malformed or unsupported version" });
|
|
1405
|
-
continue;
|
|
1406
|
-
}
|
|
1407
|
-
// Liveness is only meaningful for a lock taken on THIS host.
|
|
1408
|
-
const alive = owner.host === deps.env.HOSTNAME || owner.host === os.hostname()
|
|
1409
|
-
? isAlive(owner.owner_pid)
|
|
1410
|
-
: null;
|
|
1411
|
-
locks.push({ epic, owner_pid: owner.owner_pid, host: owner.host, alive });
|
|
1412
|
-
}
|
|
1413
|
-
}
|
|
1414
|
-
return {
|
|
1415
|
-
ghPresent,
|
|
1416
|
-
ghAuthenticated,
|
|
1417
|
-
worktrunkBinary,
|
|
1418
|
-
worktrunkAvailable,
|
|
1419
|
-
repo,
|
|
1420
|
-
bridgeCredentialResolved,
|
|
1421
|
-
stateDirectory,
|
|
1422
|
-
stateDirectoryReadable,
|
|
1423
|
-
checkpoints,
|
|
1424
|
-
locks,
|
|
1425
|
-
};
|
|
1426
|
-
}
|
|
1427
|
-
/** Render the implement-epic section. Advisory: never changes the exit code. */
|
|
1428
|
-
export function formatConductEpicDiagnosticReport(diagnostic) {
|
|
1429
|
-
const lines = ["", "implement-epic readiness (advisory)"];
|
|
1430
|
-
lines.push(diagnostic.ghAuthenticated
|
|
1431
|
-
? " OK gh: present and authenticated."
|
|
1432
|
-
: diagnostic.ghPresent
|
|
1433
|
-
? " MISSING gh: present but NOT authenticated. Run `gh auth login`."
|
|
1434
|
-
: " MISSING gh: not found on PATH. `implement-epic init` and `status` need it.");
|
|
1435
|
-
lines.push(diagnostic.worktrunkAvailable
|
|
1436
|
-
? ` OK Worktrunk: '${diagnostic.worktrunkBinary}' resolves.`
|
|
1437
|
-
: ` MISSING Worktrunk: '${diagnostic.worktrunkBinary}' not found (honors BAPI_WORKTRUNK_BIN).`);
|
|
1438
|
-
if (diagnostic.repo === null) {
|
|
1439
|
-
lines.push(" MISSING Bridge identity: the repository could not be resolved. Set BAPI_REPO_NAME\n" +
|
|
1440
|
-
" or add a valid .bridge/config at the repository root.");
|
|
1441
|
-
}
|
|
1442
|
-
else if (diagnostic.bridgeCredentialResolved) {
|
|
1443
|
-
lines.push(` OK Bridge credential for ${diagnostic.repo}: resolved.`);
|
|
1444
|
-
}
|
|
1445
|
-
else {
|
|
1446
|
-
lines.push(` MISSING Bridge credential for ${diagnostic.repo}: NOT resolved.`);
|
|
1447
|
-
}
|
|
1448
|
-
if (diagnostic.stateDirectory === null) {
|
|
1449
|
-
lines.push(" SKIP Checkpoint directory: not resolved (no repository identity).");
|
|
1450
|
-
}
|
|
1451
|
-
else if (!diagnostic.stateDirectoryReadable) {
|
|
1452
|
-
lines.push(` SKIP Checkpoint directory ${diagnostic.stateDirectory}: absent or unreadable\n` +
|
|
1453
|
-
" (normal before the first `implement-epic init`).");
|
|
1454
|
-
}
|
|
1455
|
-
else {
|
|
1456
|
-
lines.push(` OK Checkpoint directory ${diagnostic.stateDirectory}: readable, ` +
|
|
1457
|
-
`${diagnostic.checkpoints.length} checkpoint(s).`);
|
|
1458
|
-
}
|
|
1459
|
-
if (diagnostic.locks.length === 0) {
|
|
1460
|
-
lines.push(" OK Locks: none held.");
|
|
1461
|
-
}
|
|
1462
|
-
else {
|
|
1463
|
-
for (const lock of diagnostic.locks) {
|
|
1464
|
-
if (lock.problem !== undefined) {
|
|
1465
|
-
lines.push(` WARN Lock ${lock.epic}: ${lock.problem}. Inspect it by hand.`);
|
|
1466
|
-
}
|
|
1467
|
-
else if (lock.alive === false) {
|
|
1468
|
-
lines.push(` WARN Lock ${lock.epic}: owner pid ${lock.owner_pid} on ${lock.host} is gone (stale).\n` +
|
|
1469
|
-
" The next mutating verb recovers it automatically.");
|
|
1470
|
-
}
|
|
1471
|
-
else if (lock.alive === null) {
|
|
1472
|
-
lines.push(` WARN Lock ${lock.epic}: taken on host ${lock.host}; liveness is unknowable here.`);
|
|
1473
|
-
}
|
|
1474
|
-
else {
|
|
1475
|
-
lines.push(` OK Lock ${lock.epic}: held by live pid ${lock.owner_pid}.`);
|
|
1476
|
-
}
|
|
1477
|
-
}
|
|
1478
|
-
}
|
|
1479
|
-
lines.push(" Read-only: this section probes commands and reads local state. It recovers no");
|
|
1480
|
-
lines.push(" lock, rewrites no checkpoint, and never changes the exit code.");
|
|
1481
|
-
return lines.join("\n");
|
|
1482
|
-
}
|
|
1483
1314
|
/**
|
|
1484
1315
|
* The transitional local helpers BAPI-1102 retires.
|
|
1485
1316
|
*
|
|
@@ -1501,14 +1332,10 @@ export const LEGACY_DRIVE_HELPER_RELATIVE_PATHS = [
|
|
|
1501
1332
|
/**
|
|
1502
1333
|
* Collect the operational advisories, read-only.
|
|
1503
1334
|
*
|
|
1504
|
-
*
|
|
1505
|
-
*
|
|
1506
|
-
* injected liveness seam — never from the process table. Only a lock proven live
|
|
1507
|
-
* on THIS host counts; malformed, unreadable, dead, and remote-host locks are
|
|
1508
|
-
* excluded from the live set, with the indeterminate ones counted separately.
|
|
1335
|
+
* Concurrent conductors and parse-dispatcher health are read from two read-only
|
|
1336
|
+
* Bridge GETs; no local process table or lock directory is inspected.
|
|
1509
1337
|
*/
|
|
1510
1338
|
export async function collectOperationalAdvisories(deps) {
|
|
1511
|
-
const isAlive = deps.isProcessAlive ?? isConductEpicLockOwnerAlive;
|
|
1512
1339
|
const now = deps.now ?? (() => new Date());
|
|
1513
1340
|
const resolveAccess = deps.resolveAccess ?? resolveConductorBridgeApiAccess;
|
|
1514
1341
|
let dispatcher = null;
|
|
@@ -1542,63 +1369,6 @@ export async function collectOperationalAdvisories(deps) {
|
|
|
1542
1369
|
conductors.push(...live.value.conductors);
|
|
1543
1370
|
}
|
|
1544
1371
|
}
|
|
1545
|
-
// Local pilot locks. Read through the same path the readiness section uses; a
|
|
1546
|
-
// failure to read the directory is silence, not a claim that none are held.
|
|
1547
|
-
let indeterminateLocks = 0;
|
|
1548
|
-
if (repo !== null) {
|
|
1549
|
-
const stateDirectory = resolveConductEpicStateDirectory(repo, {
|
|
1550
|
-
env: deps.env,
|
|
1551
|
-
homedir: deps.homedir,
|
|
1552
|
-
});
|
|
1553
|
-
let entries = [];
|
|
1554
|
-
try {
|
|
1555
|
-
entries = await deps.readdir(stateDirectory);
|
|
1556
|
-
}
|
|
1557
|
-
catch {
|
|
1558
|
-
entries = [];
|
|
1559
|
-
}
|
|
1560
|
-
for (const entry of entries) {
|
|
1561
|
-
if (!entry.endsWith(".lock"))
|
|
1562
|
-
continue;
|
|
1563
|
-
const epic = entry.slice(0, -".lock".length);
|
|
1564
|
-
let raw;
|
|
1565
|
-
try {
|
|
1566
|
-
raw = await deps.readFile(path.join(stateDirectory, entry));
|
|
1567
|
-
}
|
|
1568
|
-
catch {
|
|
1569
|
-
indeterminateLocks += 1;
|
|
1570
|
-
continue;
|
|
1571
|
-
}
|
|
1572
|
-
const owner = parseConductEpicLock(raw);
|
|
1573
|
-
if (owner === null) {
|
|
1574
|
-
indeterminateLocks += 1;
|
|
1575
|
-
continue;
|
|
1576
|
-
}
|
|
1577
|
-
const local = owner.host === deps.env.HOSTNAME || owner.host === os.hostname();
|
|
1578
|
-
if (!local) {
|
|
1579
|
-
indeterminateLocks += 1;
|
|
1580
|
-
continue;
|
|
1581
|
-
}
|
|
1582
|
-
if (!isAlive(owner.owner_pid))
|
|
1583
|
-
continue;
|
|
1584
|
-
// A pilot lock and a v2 run are two DISTINCT conductor authorities. Even
|
|
1585
|
-
// when both name the same epic they are not deduplicated — that pairing is
|
|
1586
|
-
// precisely the overlap an operator most needs to see.
|
|
1587
|
-
//
|
|
1588
|
-
// The key comes from a FILENAME, so it is bounded against the epic-key shape
|
|
1589
|
-
// before it is rendered; an unrecognized name reports `unknown` rather than
|
|
1590
|
-
// putting arbitrary filesystem text into a diagnostic line.
|
|
1591
|
-
conductors.push({
|
|
1592
|
-
kind: "pilot",
|
|
1593
|
-
epicKey: validateTicketKey(epic, "epic key").ok ? epic : null,
|
|
1594
|
-
state: "lock held",
|
|
1595
|
-
livenessSource: "lock",
|
|
1596
|
-
});
|
|
1597
|
-
}
|
|
1598
|
-
if (conductors.length > 0 && conductorObservation !== "observed") {
|
|
1599
|
-
conductorObservation = "observed";
|
|
1600
|
-
}
|
|
1601
|
-
}
|
|
1602
1372
|
// BAPI-1102 — a pure path-presence check, using the SAME injected `stat` and
|
|
1603
1373
|
// `homedir` seams every other advisory here uses, so it stays deterministic
|
|
1604
1374
|
// under test and reaches no real filesystem in one. A `stat` that throws is
|
|
@@ -1620,7 +1390,6 @@ export async function collectOperationalAdvisories(deps) {
|
|
|
1620
1390
|
dispatcher,
|
|
1621
1391
|
conductorObservation,
|
|
1622
1392
|
conductors,
|
|
1623
|
-
indeterminateLocks,
|
|
1624
1393
|
legacyDriveHelpers,
|
|
1625
1394
|
};
|
|
1626
1395
|
}
|
|
@@ -1658,10 +1427,6 @@ export function formatOperationalAdvisoryReport(diagnostic) {
|
|
|
1658
1427
|
lines.push(" A distinct-epic overlap is informational and may be legitimate: separately\n" +
|
|
1659
1428
|
" scoped epics do not share transition authority.");
|
|
1660
1429
|
}
|
|
1661
|
-
if (diagnostic.indeterminateLocks > 0) {
|
|
1662
|
-
lines.push(` ${diagnostic.indeterminateLocks} local lock(s) could not be judged (remote host,\n` +
|
|
1663
|
-
" malformed, or unreadable) and are excluded from the live count.");
|
|
1664
|
-
}
|
|
1665
1430
|
// BAPI-1102. A WARN, never a failure, and deliberately separate from every
|
|
1666
1431
|
// readiness signal above: a stale helper says nothing about whether this
|
|
1667
1432
|
// repository's conductor works, so classifying it as a readiness problem would
|
|
@@ -1677,7 +1442,7 @@ export function formatOperationalAdvisoryReport(diagnostic) {
|
|
|
1677
1442
|
" doctor performed a PATH-PRESENCE check only: it did not run, read, or\n" +
|
|
1678
1443
|
" modify either file, and it cannot delete them for you.");
|
|
1679
1444
|
}
|
|
1680
|
-
lines.push(" Read-only: this section issues two Bridge GETs and reads local
|
|
1445
|
+
lines.push(" Read-only: this section issues two Bridge GETs and reads no local state. It");
|
|
1681
1446
|
lines.push(" runs NO command probe, repairs no scheduler, and never changes the exit code.");
|
|
1682
1447
|
return lines.join("\n");
|
|
1683
1448
|
}
|
|
@@ -1748,8 +1513,17 @@ export async function runDoctorCli(argv, overrides = {}) {
|
|
|
1748
1513
|
const profile = await inspectBridgeCodexProfile(codexHome(deps.env), readCodexFile);
|
|
1749
1514
|
log(profile.status === "active"
|
|
1750
1515
|
? "Codex Bridge approval profile installed; spawned workers use --profile bridge."
|
|
1751
|
-
:
|
|
1752
|
-
|
|
1516
|
+
: profile.status === "stale"
|
|
1517
|
+
? "Codex Bridge approval profile is an older unmodified managed version; the next spawned worker refreshes it before launch."
|
|
1518
|
+
: `Codex Bridge approval profile ${profile.status}; spawned workers use existing Codex approvals. Run install --tools=codex --codex-profile=on to opt in.`);
|
|
1519
|
+
}
|
|
1520
|
+
// Read-only personal routing diagnostics. The installer marker is a hint;
|
|
1521
|
+
// an initialized MCP client identity takes precedence during real requests.
|
|
1522
|
+
const routingHost = resolveRoutingHost(undefined, deps.env.BAPI_MCP_HOST_FAMILY);
|
|
1523
|
+
log(`Model routing host marker: ${routingHost.warning ?? routingHost.family ?? "neutral"}.`);
|
|
1524
|
+
const preferencePath = modelRoutingPreferencesPath(deps.homedir?.() ?? os.homedir());
|
|
1525
|
+
const preference = await readModelRoutingPreference(deps.env.BAPI_REPO_NAME ?? "_doctor_unscoped_", "review_ticket", preferencePath);
|
|
1526
|
+
log(`Personal model routing preference: ${preference.warning ?? preference.source}.`);
|
|
1753
1527
|
log(formatDoctorPrereqSection(deps.platform, collection));
|
|
1754
1528
|
// Executor-adapter section (BAPI-781). Advisory with respect to the EXIT CODE
|
|
1755
1529
|
// — an unusable adapter is reported loudly but does not change doctor's
|
|
@@ -1971,55 +1745,8 @@ export async function runDoctorCli(argv, overrides = {}) {
|
|
|
1971
1745
|
}));
|
|
1972
1746
|
}
|
|
1973
1747
|
}
|
|
1974
|
-
// Advisory
|
|
1975
|
-
//
|
|
1976
|
-
// these same invariants at the point where they actually block, and someone who
|
|
1977
|
-
// never conducts an epic should not see a failing doctor because of it.
|
|
1978
|
-
//
|
|
1979
|
-
// Skipped entirely on an unsupported platform. This section is the only
|
|
1980
|
-
// advisory one that runs COMMAND probes (`gh`, Worktrunk), and doctor's
|
|
1981
|
-
// standing contract is that an unsupported platform probes nothing at all —
|
|
1982
|
-
// reporting on tooling for a `implement-epic` that cannot run there would buy
|
|
1983
|
-
// nothing and break that contract.
|
|
1984
|
-
if (overrides.conductEpic !== false && collection.ok) {
|
|
1985
|
-
try {
|
|
1986
|
-
const injectedFs = deps;
|
|
1987
|
-
const conductDeps = {
|
|
1988
|
-
env: overrides.conductEpic?.env ?? deps.env,
|
|
1989
|
-
cwd: overrides.conductEpic?.cwd ?? deps.cwd,
|
|
1990
|
-
platform: overrides.conductEpic?.platform ?? deps.platform,
|
|
1991
|
-
homedir: overrides.conductEpic?.homedir ?? injectedFs.homedir ?? os.homedir,
|
|
1992
|
-
readFile: overrides.conductEpic?.readFile ??
|
|
1993
|
-
injectedFs.readFile ??
|
|
1994
|
-
((p) => readFile(p, "utf-8")),
|
|
1995
|
-
stat: overrides.conductEpic?.stat ?? injectedFs.stat ?? ((p) => stat(p)),
|
|
1996
|
-
readdir: overrides.conductEpic?.readdir ?? ((p) => readdir(p)),
|
|
1997
|
-
runCommand: overrides.conductEpic?.runCommand ?? deps.runCommand,
|
|
1998
|
-
resolveAccess: overrides.conductEpic?.resolveAccess,
|
|
1999
|
-
isProcessAlive: overrides.conductEpic?.isProcessAlive,
|
|
2000
|
-
};
|
|
2001
|
-
log(formatConductEpicDiagnosticReport(await collectConductEpicDiagnostic(conductDeps)));
|
|
2002
|
-
}
|
|
2003
|
-
catch {
|
|
2004
|
-
// Any unexpected failure still renders a sanitized advisory section rather
|
|
2005
|
-
// than dropping it or leaking exception text.
|
|
2006
|
-
log(formatConductEpicDiagnosticReport({
|
|
2007
|
-
ghPresent: false,
|
|
2008
|
-
ghAuthenticated: false,
|
|
2009
|
-
worktrunkBinary: resolveWorktrunkBinary(deps.platform, deps.env),
|
|
2010
|
-
worktrunkAvailable: false,
|
|
2011
|
-
repo: null,
|
|
2012
|
-
bridgeCredentialResolved: false,
|
|
2013
|
-
stateDirectory: null,
|
|
2014
|
-
stateDirectoryReadable: false,
|
|
2015
|
-
checkpoints: [],
|
|
2016
|
-
locks: [],
|
|
2017
|
-
}));
|
|
2018
|
-
}
|
|
2019
|
-
}
|
|
2020
|
-
// Advisory operational-advisories section (BAPI-963), placed immediately after
|
|
2021
|
-
// `implement-epic readiness` because both are read by the same operator at the
|
|
2022
|
-
// same moment — "can I conduct?" then "what else is already running?".
|
|
1748
|
+
// Advisory operational-advisories section (BAPI-963). Read by the operator
|
|
1749
|
+
// asking "what is already running?" right after the readiness sections above.
|
|
2023
1750
|
//
|
|
2024
1751
|
// Gated on `collection.ok` for the same reason the section above is: an
|
|
2025
1752
|
// unsupported platform reports nothing. That gate is belt-and-braces here,
|
|
@@ -2042,9 +1769,7 @@ export async function runDoctorCli(argv, overrides = {}) {
|
|
|
2042
1769
|
injectedFs.readFile ??
|
|
2043
1770
|
((p) => readFile(p, "utf-8")),
|
|
2044
1771
|
stat: overrides.operationalAdvisories?.stat ?? injectedFs.stat ?? ((p) => stat(p)),
|
|
2045
|
-
readdir: overrides.operationalAdvisories?.readdir ?? ((p) => readdir(p)),
|
|
2046
1772
|
resolveAccess: overrides.operationalAdvisories?.resolveAccess,
|
|
2047
|
-
isProcessAlive: overrides.operationalAdvisories?.isProcessAlive,
|
|
2048
1773
|
fetchImpl: overrides.operationalAdvisories?.fetchImpl,
|
|
2049
1774
|
now: overrides.operationalAdvisories?.now,
|
|
2050
1775
|
};
|
|
@@ -2057,7 +1782,6 @@ export async function runDoctorCli(argv, overrides = {}) {
|
|
|
2057
1782
|
dispatcher: null,
|
|
2058
1783
|
conductorObservation: "unavailable",
|
|
2059
1784
|
conductors: [],
|
|
2060
|
-
indeterminateLocks: 0,
|
|
2061
1785
|
// Empty, not omitted: a collection that failed has not established
|
|
2062
1786
|
// that a legacy helper is absent, and a fallback that CLAIMED to have
|
|
2063
1787
|
// checked would be the one kind of wrong answer worse than silence.
|
package/build/drive-epic.js
CHANGED
|
@@ -1,32 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `drive-epic` — the one universal conductor entry point (BAPI-887).
|
|
3
3
|
*
|
|
4
|
-
* Bridge has
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* `epic_run` row, stores a plan DAG, and approves it.
|
|
9
|
-
* - **LLM-conductor pilot** — `/loop 5m /conduct-epic <EPIC>`, which creates no
|
|
10
|
-
* run row and keeps its whole memory in a local checkpoint plus GitHub.
|
|
4
|
+
* Bridge has one conductor — the **v2 engine**, bootstrapped by `setup-epic`,
|
|
5
|
+
* which creates a durable `epic_run` row, stores a plan DAG, and approves it. The
|
|
6
|
+
* standing rule is that exactly one transition authority ever operates on an
|
|
7
|
+
* epic: two authorities on one epic wedge it permanently.
|
|
11
8
|
*
|
|
12
9
|
* Asking a prompt to pick correctly every time is not a control. This subcommand
|
|
13
10
|
* makes the choice STRUCTURAL: it reads server-side conductor readiness and
|
|
14
|
-
* routes to exactly one path, so no prompt names
|
|
15
|
-
*
|
|
16
|
-
* `drive-epic` and to nothing else.
|
|
11
|
+
* routes to exactly one path, so no prompt names the underlying conductor. Every
|
|
12
|
+
* ticket-authoring surface hands off to `drive-epic` and to nothing else.
|
|
17
13
|
*
|
|
18
14
|
* ## The output invariant
|
|
19
15
|
*
|
|
20
16
|
* Every branch — including every error path — emits **at most one** conductor
|
|
21
|
-
* invocation.
|
|
22
|
-
* try
|
|
23
|
-
* exists to remove.
|
|
24
|
-
*
|
|
17
|
+
* invocation. A not-ready result or an escalation carries NO alternative
|
|
18
|
+
* conductor command: "v2 failed, try something else instead" is precisely the
|
|
19
|
+
* dual-authority hazard this subcommand exists to remove.
|
|
20
|
+
* {@link assertSingleConductorInvocation} enforces this on the way out rather than
|
|
21
|
+
* trusting each branch to have got it right.
|
|
25
22
|
*
|
|
26
23
|
* ## Three-state routing, fail-closed
|
|
27
24
|
*
|
|
28
25
|
* - readiness green -> delegate to the `setup-epic` (v2) flow
|
|
29
|
-
* - readiness read, not green ->
|
|
26
|
+
* - readiness read, not green -> report v2 not-ready, offer no alternative
|
|
30
27
|
* - readiness UNKNOWN -> escalate to the operator, print no conductor
|
|
31
28
|
*
|
|
32
29
|
* Unknown is never treated as not-ready, and selection never happens by
|
|
@@ -72,17 +69,16 @@ import { validateBranchName } from "./base-ref.js";
|
|
|
72
69
|
// BAPI-806: mcp-identity.ts is the SOLE source of the package-name literal, so
|
|
73
70
|
// the printed invocation interpolates it rather than repeating it.
|
|
74
71
|
import { MCP_PACKAGE_NAME } from "./mcp-identity.js";
|
|
75
|
-
/** Jira epic key shape. Matches
|
|
72
|
+
/** Jira epic key shape. Matches the epic-key positional rule exactly. */
|
|
76
73
|
const EPIC_KEY_PATTERN = /^[A-Z]+-[0-9]+$/;
|
|
77
|
-
/** The
|
|
78
|
-
export const DRIVE_EPIC_CONDUCTORS = ["v2"
|
|
74
|
+
/** The conductor paths. Derived types below come from this constant. */
|
|
75
|
+
export const DRIVE_EPIC_CONDUCTORS = ["v2"];
|
|
79
76
|
/**
|
|
80
|
-
*
|
|
77
|
+
* The preferred conductor.
|
|
81
78
|
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
* than
|
|
85
|
-
* eliminating a conductor is a one-line change plus one deleted branch.
|
|
79
|
+
* v2 is the only conductor: when readiness is green it drives the epic, and when
|
|
80
|
+
* it is not the command reports v2 not-ready with no alternative. Expressed as
|
|
81
|
+
* one named constant rather than branching scattered through the routing logic.
|
|
86
82
|
*/
|
|
87
83
|
export const DRIVE_EPIC_PREFERRED_CONDUCTOR = "v2";
|
|
88
84
|
/**
|
|
@@ -482,10 +478,10 @@ export function parseDriveEpicArgs(argv) {
|
|
|
482
478
|
* v2 engine drive an epic at all?", and the answer does not depend on whether
|
|
483
479
|
* the repository has consented to UNATTENDED operation: an attended v2 run on a
|
|
484
480
|
* non-consenting repository is a perfectly ordinary, fully supported run.
|
|
485
|
-
* Including them would
|
|
486
|
-
*
|
|
487
|
-
*
|
|
488
|
-
*
|
|
481
|
+
* Including them would report v2 not-ready for every such repository the moment
|
|
482
|
+
* this ticket deployed — a silent, repository-wide refusal for a reason that has
|
|
483
|
+
* nothing to do with readiness, and the exact opposite of AC-3's "nothing
|
|
484
|
+
* changes for an existing run".
|
|
489
485
|
*
|
|
490
486
|
* They still gate the COMPOSED bring-up, through
|
|
491
487
|
* {@link classifyV2Readiness}, which is where an unattended run is actually
|
|
@@ -501,8 +497,7 @@ export function selectConductor(readiness) {
|
|
|
501
497
|
};
|
|
502
498
|
}
|
|
503
499
|
return {
|
|
504
|
-
kind: "
|
|
505
|
-
conductor: "pilot",
|
|
500
|
+
kind: "not_ready",
|
|
506
501
|
reason: `the engine path still needs ${missing.map((m) => m.describe).join("; ")}`,
|
|
507
502
|
};
|
|
508
503
|
}
|
|
@@ -536,51 +531,35 @@ export function buildSetupEpicArgv(options) {
|
|
|
536
531
|
];
|
|
537
532
|
}
|
|
538
533
|
/**
|
|
539
|
-
* Render the selected path and only
|
|
540
|
-
*
|
|
541
|
-
* Precondition: the caller has already ruled out delegation. The `v2` branch
|
|
542
|
-
* reads as "green, but no plan file was supplied", which is only true once
|
|
543
|
-
* {@link runDriveEpicCli} has declined to delegate — so route first, render
|
|
544
|
-
* second.
|
|
534
|
+
* Render the selected v2 path and only that path.
|
|
545
535
|
*
|
|
546
|
-
*
|
|
547
|
-
*
|
|
548
|
-
*
|
|
536
|
+
* Precondition: the caller has already ruled out delegation and a not-ready
|
|
537
|
+
* result. The `v2` branch reads as "green, but no plan file was supplied", which
|
|
538
|
+
* is only true once {@link runDriveEpicCli} has declined to delegate — so route
|
|
539
|
+
* first, render second.
|
|
549
540
|
*/
|
|
550
541
|
export function renderConductorHandoff(selection, epicKey) {
|
|
551
|
-
|
|
552
|
-
const invocation = `npx -y ${MCP_PACKAGE_NAME} drive-epic ${epicKey} --plan-file <path>`;
|
|
553
|
-
return {
|
|
554
|
-
conductor: "v2",
|
|
555
|
-
invocation,
|
|
556
|
-
lines: [
|
|
557
|
-
`${epicKey}: ${selection.reason}, but no plan file was supplied.`,
|
|
558
|
-
"",
|
|
559
|
-
"The engine path needs a plan DAG, and nothing derives one from an epic key:",
|
|
560
|
-
"the dependency edges and per-ticket touched_files it carries do not exist",
|
|
561
|
-
"anywhere else. Produce one, then re-run this command with it.",
|
|
562
|
-
"",
|
|
563
|
-
` 1. Run /plan-epic ${epicKey} in an interactive session. It writes the`,
|
|
564
|
-
" sidecar to {docs_dir}/epic-plans/{epic_slug}/epic-plan.dag.json.",
|
|
565
|
-
" If the epic's tickets already exist, run emit-epic-implementer-bundle",
|
|
566
|
-
" finalize instead — it resolves the placeholder keys in an existing",
|
|
567
|
-
" sidecar and attaches touched_files.",
|
|
568
|
-
"",
|
|
569
|
-
` 2. ${invocation}`,
|
|
570
|
-
"",
|
|
571
|
-
`That second command bootstraps the run for you; ${SETUP_EPIC_SUBCOMMAND} is not`,
|
|
572
|
-
"something you invoke directly.",
|
|
573
|
-
],
|
|
574
|
-
};
|
|
575
|
-
}
|
|
576
|
-
const invocation = `/loop 5m /conduct-epic ${epicKey}`;
|
|
542
|
+
const invocation = `npx -y ${MCP_PACKAGE_NAME} drive-epic ${epicKey} --plan-file <path>`;
|
|
577
543
|
return {
|
|
578
|
-
conductor: "
|
|
544
|
+
conductor: "v2",
|
|
579
545
|
invocation,
|
|
580
546
|
lines: [
|
|
581
|
-
`${epicKey}: ${selection.reason}
|
|
547
|
+
`${epicKey}: ${selection.reason}, but no plan file was supplied.`,
|
|
548
|
+
"",
|
|
549
|
+
"The engine path needs a plan DAG, and nothing derives one from an epic key:",
|
|
550
|
+
"the dependency edges and per-ticket touched_files it carries do not exist",
|
|
551
|
+
"anywhere else. Produce one, then re-run this command with it.",
|
|
552
|
+
"",
|
|
553
|
+
` 1. Run /plan-epic ${epicKey} in an interactive session. It writes the`,
|
|
554
|
+
" sidecar to {docs_dir}/epic-plans/{epic_slug}/epic-plan.dag.json.",
|
|
555
|
+
" If the epic's tickets already exist, run emit-epic-implementer-bundle",
|
|
556
|
+
" finalize instead — it resolves the placeholder keys in an existing",
|
|
557
|
+
" sidecar and attaches touched_files.",
|
|
582
558
|
"",
|
|
583
|
-
` ${invocation}`,
|
|
559
|
+
` 2. ${invocation}`,
|
|
560
|
+
"",
|
|
561
|
+
`That second command bootstraps the run for you; ${SETUP_EPIC_SUBCOMMAND} is not`,
|
|
562
|
+
"something you invoke directly.",
|
|
584
563
|
],
|
|
585
564
|
};
|
|
586
565
|
}
|
|
@@ -589,7 +568,7 @@ export function renderConductorHandoff(selection, epicKey) {
|
|
|
589
568
|
* output guard and by its test — one list, so a new spelling cannot be added to
|
|
590
569
|
* the renderer without the guard seeing it.
|
|
591
570
|
*/
|
|
592
|
-
export const CONDUCTOR_INVOCATION_TOKENS = ["setup-epic"
|
|
571
|
+
export const CONDUCTOR_INVOCATION_TOKENS = ["setup-epic"];
|
|
593
572
|
/** The v2 token, named once so prose and the guard cannot drift apart. */
|
|
594
573
|
const SETUP_EPIC_SUBCOMMAND = CONDUCTOR_INVOCATION_TOKENS[0];
|
|
595
574
|
/**
|
|
@@ -935,9 +914,9 @@ export async function runDriveEpicCli(argv, overrides = {}) {
|
|
|
935
914
|
//
|
|
936
915
|
// BAPI-1054: a supplied plan file is what makes this a BRING-UP decision
|
|
937
916
|
// rather than a routing one. Everything below runs before `selectConductor`'s
|
|
938
|
-
//
|
|
939
|
-
// the runtime facts is not a "
|
|
940
|
-
//
|
|
917
|
+
// not-ready result can be rendered, because a report that is non-green only on
|
|
918
|
+
// the runtime facts is not a "v2 cannot run" situation — it is a plane that
|
|
919
|
+
// has not been started yet.
|
|
941
920
|
if (planFile) {
|
|
942
921
|
// BAPI-1102 — the explicit `--attended` opt-out makes the unattended
|
|
943
922
|
// prerequisites inapplicable. Both inputs are FLAG checks, not policy
|
|
@@ -1061,6 +1040,21 @@ export async function runDriveEpicCli(argv, overrides = {}) {
|
|
|
1061
1040
|
awaitPlaneReady: controlPlane.awaitPlaneReady,
|
|
1062
1041
|
});
|
|
1063
1042
|
}
|
|
1043
|
+
// No plan file, and readiness is not green: v2 is not ready and there is no
|
|
1044
|
+
// second conductor to offer. Report it as a not-ready refusal on stderr with
|
|
1045
|
+
// exactly one — actually zero — conductor invocation, and a non-zero exit.
|
|
1046
|
+
if (selection.kind === "not_ready") {
|
|
1047
|
+
const refusal = [
|
|
1048
|
+
`${epicKey}: ${selection.reason}.`,
|
|
1049
|
+
"",
|
|
1050
|
+
"The v2 engine is not ready to drive this epic, and there is no alternative",
|
|
1051
|
+
"conductor to fall back to. Resolve conductor readiness (`install-bridge",
|
|
1052
|
+
"conductor`, then `doctor`) and run drive-epic again.",
|
|
1053
|
+
].join("\n");
|
|
1054
|
+
assertSingleConductorInvocation(refusal);
|
|
1055
|
+
deps.errorLog(refusal);
|
|
1056
|
+
return 1;
|
|
1057
|
+
}
|
|
1064
1058
|
const handoff = renderConductorHandoff(selection, epicKey);
|
|
1065
1059
|
const text = handoff.lines.join("\n");
|
|
1066
1060
|
assertSingleConductorInvocation(text);
|