@bridge_gpt/mcp-server 0.2.12 → 0.2.14
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/CONDUCTOR.md +131 -0
- package/README.md +125 -103
- package/build/conductor/doctor.js +78 -1
- package/build/conductor/epic-runtime.js +126 -54
- package/build/conductor/epic-state.js +20 -5
- package/build/conductor/local-merge.js +212 -0
- package/build/conductor/pr-ci-producer.js +12 -2
- package/build/conductor/store.js +7 -4
- package/build/conductor/taxonomy.js +4 -0
- package/build/conductor-bin.js +476 -137
- package/build/doctor.js +3 -0
- package/build/index.js +1818 -441
- package/build/init.js +57 -0
- package/build/mcp-profile.js +33 -30
- package/build/readme.generated.js +1 -1
- package/build/sfcc/client.js +151 -0
- package/build/sfcc/config.js +39 -0
- package/build/sfcc/credentials.js +136 -0
- package/build/sfcc/ocapi-shape.js +77 -0
- package/build/sfcc/output.js +39 -0
- package/build/sfcc/permissions.js +136 -0
- package/build/sfcc/reads-custom-object-def.js +119 -0
- package/build/sfcc/reads-site-preference.js +158 -0
- package/build/sfcc/reads-system-object.js +162 -0
- package/build/sfcc/register.js +73 -0
- package/build/sfcc/setup-status.js +114 -0
- package/build/sfcc/tool-wrapper.js +70 -0
- package/build/start-tickets-conductor.js +9 -1
- package/build/start-tickets.js +47 -4
- package/build/version.generated.js +1 -1
- package/package.json +3 -2
package/build/index.js
CHANGED
|
@@ -14,7 +14,7 @@ var VERSION;
|
|
|
14
14
|
var init_version_generated = __esm({
|
|
15
15
|
"src/version.generated.ts"() {
|
|
16
16
|
"use strict";
|
|
17
|
-
VERSION = "0.2.
|
|
17
|
+
VERSION = "0.2.14";
|
|
18
18
|
}
|
|
19
19
|
});
|
|
20
20
|
|
|
@@ -1098,14 +1098,14 @@ function credentialResolutionDescriptor() {
|
|
|
1098
1098
|
label: "Bridge API credential resolution",
|
|
1099
1099
|
installHint: CREDENTIAL_RESOLUTION_INSTALL_HINTS,
|
|
1100
1100
|
probe: async (deps) => {
|
|
1101
|
-
const { readFile:
|
|
1102
|
-
if (!
|
|
1101
|
+
const { readFile: readFile12, stat: stat9, homedir } = deps;
|
|
1102
|
+
if (!readFile12 || !stat9 || !homedir) {
|
|
1103
1103
|
return { found: false, detail: "credential probe unavailable (no read-only filesystem access)" };
|
|
1104
1104
|
}
|
|
1105
1105
|
const repoName = await resolveStartTicketsRepoName({
|
|
1106
1106
|
env: deps.env,
|
|
1107
1107
|
cwd: deps.cwd,
|
|
1108
|
-
readFile:
|
|
1108
|
+
readFile: readFile12
|
|
1109
1109
|
});
|
|
1110
1110
|
if (!repoName) {
|
|
1111
1111
|
return {
|
|
@@ -1118,7 +1118,7 @@ function credentialResolutionDescriptor() {
|
|
|
1118
1118
|
env: deps.env,
|
|
1119
1119
|
homedir,
|
|
1120
1120
|
platform: deps.platform,
|
|
1121
|
-
readFile:
|
|
1121
|
+
readFile: readFile12,
|
|
1122
1122
|
stat: stat9
|
|
1123
1123
|
});
|
|
1124
1124
|
if (result.ok) {
|
|
@@ -1138,11 +1138,11 @@ function worktreeMcpReachabilityDescriptor() {
|
|
|
1138
1138
|
label: "Worktree MCP registration reachability",
|
|
1139
1139
|
installHint: WORKTREE_MCP_INSTALL_HINTS,
|
|
1140
1140
|
probe: async (deps) => {
|
|
1141
|
-
const { readFile:
|
|
1142
|
-
if (!
|
|
1141
|
+
const { readFile: readFile12 } = deps;
|
|
1142
|
+
if (!readFile12) {
|
|
1143
1143
|
return { found: false, detail: "registration probe unavailable (no read-only filesystem access)" };
|
|
1144
1144
|
}
|
|
1145
|
-
const result = await probeWorktreeMcpRegistration(deps.cwd, { readFile:
|
|
1145
|
+
const result = await probeWorktreeMcpRegistration(deps.cwd, { readFile: readFile12 });
|
|
1146
1146
|
return { found: result.found, detail: result.detail };
|
|
1147
1147
|
}
|
|
1148
1148
|
};
|
|
@@ -1372,6 +1372,36 @@ var init_agent_registry = __esm({
|
|
|
1372
1372
|
}
|
|
1373
1373
|
});
|
|
1374
1374
|
|
|
1375
|
+
// src/mcp-profile.ts
|
|
1376
|
+
function resolveProfiles(raw) {
|
|
1377
|
+
const baseline = /* @__PURE__ */ new Set(["core"]);
|
|
1378
|
+
if (raw === void 0) return baseline;
|
|
1379
|
+
const trimmed = raw.trim();
|
|
1380
|
+
if (trimmed === "") return baseline;
|
|
1381
|
+
const tokens = trimmed.split(",").map((t) => t.trim().toLowerCase());
|
|
1382
|
+
if (tokens.some((t) => t === "full")) {
|
|
1383
|
+
return /* @__PURE__ */ new Set(["core", "conductor", "pipeline-authoring", "sfcc"]);
|
|
1384
|
+
}
|
|
1385
|
+
for (const token of tokens) {
|
|
1386
|
+
if (VALID_GROUPS.has(token)) {
|
|
1387
|
+
baseline.add(token);
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
return baseline;
|
|
1391
|
+
}
|
|
1392
|
+
var VALID_GROUPS;
|
|
1393
|
+
var init_mcp_profile = __esm({
|
|
1394
|
+
"src/mcp-profile.ts"() {
|
|
1395
|
+
"use strict";
|
|
1396
|
+
VALID_GROUPS = /* @__PURE__ */ new Set([
|
|
1397
|
+
"core",
|
|
1398
|
+
"conductor",
|
|
1399
|
+
"pipeline-authoring",
|
|
1400
|
+
"sfcc"
|
|
1401
|
+
]);
|
|
1402
|
+
}
|
|
1403
|
+
});
|
|
1404
|
+
|
|
1375
1405
|
// src/conductor/redaction.ts
|
|
1376
1406
|
function redactSecretString(value) {
|
|
1377
1407
|
let out = value;
|
|
@@ -1557,7 +1587,11 @@ var init_taxonomy = __esm({
|
|
|
1557
1587
|
// distinct from the review.* PR-review verdicts above so the conductor never
|
|
1558
1588
|
// mis-folds a spec-review outcome as the implementation PR's review state.
|
|
1559
1589
|
"spec_review.passed",
|
|
1560
|
-
"spec_review.changes_requested"
|
|
1590
|
+
"spec_review.changes_requested",
|
|
1591
|
+
// Durable parse-after-merge marker. Emitted by epic-tick when it triggers a
|
|
1592
|
+
// post-merge repository re-index, so the (stateless) reconcile loop can fold a
|
|
1593
|
+
// merged ticket to `done` from the ledger instead of an in-memory wait map.
|
|
1594
|
+
"parse.triggered"
|
|
1561
1595
|
];
|
|
1562
1596
|
}
|
|
1563
1597
|
});
|
|
@@ -2737,7 +2771,7 @@ var init_store = __esm({
|
|
|
2737
2771
|
WAIT_TIMEOUT_MAX_MS = 12e4;
|
|
2738
2772
|
WAIT_POLL_INTERVAL_MS = 500;
|
|
2739
2773
|
SUMMARY_FIELD_MAX_CHARS = 500;
|
|
2740
|
-
CURRENT_CONDUCTOR_SCHEMA_VERSION =
|
|
2774
|
+
CURRENT_CONDUCTOR_SCHEMA_VERSION = 7;
|
|
2741
2775
|
MESSAGE_TYPE_PATTERN = /^[A-Za-z0-9._:-]{1,100}$/;
|
|
2742
2776
|
}
|
|
2743
2777
|
});
|
|
@@ -2803,9 +2837,12 @@ function isConductorFlagEnabled(value) {
|
|
|
2803
2837
|
return v === "1" || v === "true";
|
|
2804
2838
|
}
|
|
2805
2839
|
function buildConductorWorkerEnv(context, worker, parentEnv) {
|
|
2840
|
+
const parentActiveGroups = new Set(resolveProfiles(parentEnv.BRIDGE_MCP_PROFILE));
|
|
2841
|
+
parentActiveGroups.add("conductor");
|
|
2842
|
+
const mergedProfile = Array.from(parentActiveGroups).join(",");
|
|
2806
2843
|
const env = {
|
|
2807
2844
|
BAPI_CONDUCTOR_ENABLED: "1",
|
|
2808
|
-
BRIDGE_MCP_PROFILE:
|
|
2845
|
+
BRIDGE_MCP_PROFILE: mergedProfile,
|
|
2809
2846
|
BAPI_CONDUCTOR_RUN_ID: context.runId,
|
|
2810
2847
|
BAPI_CONDUCTOR_WORKER_ID: worker.workerId,
|
|
2811
2848
|
BAPI_CONDUCTOR_TICKET_KEY: worker.ticketKey,
|
|
@@ -3060,6 +3097,7 @@ var DEFAULT_CONDUCTOR_GATE_NAME, CONDUCTOR_TUNING_ENV_KEYS, CONDUCTOR_HOOK_LIFEC
|
|
|
3060
3097
|
var init_start_tickets_conductor = __esm({
|
|
3061
3098
|
"src/start-tickets-conductor.ts"() {
|
|
3062
3099
|
"use strict";
|
|
3100
|
+
init_mcp_profile();
|
|
3063
3101
|
init_start_tickets_repo();
|
|
3064
3102
|
DEFAULT_CONDUCTOR_GATE_NAME = "implement-ticket";
|
|
3065
3103
|
CONDUCTOR_TUNING_ENV_KEYS = [
|
|
@@ -3327,8 +3365,8 @@ async function fetchPrReviewStatus(access, prNumber, fetchImpl = globalThis.fetc
|
|
|
3327
3365
|
}
|
|
3328
3366
|
function buildConductorVcsUrl(baseUrl, apiPath) {
|
|
3329
3367
|
const trimmed = baseUrl.replace(/\/+$/, "");
|
|
3330
|
-
const
|
|
3331
|
-
return new URL(`${trimmed}${
|
|
3368
|
+
const path27 = apiPath.startsWith("/") ? apiPath : `/${apiPath}`;
|
|
3369
|
+
return new URL(`${trimmed}${path27}`).toString();
|
|
3332
3370
|
}
|
|
3333
3371
|
function conductorPostHeaders(access) {
|
|
3334
3372
|
return { "X-API-Key": access.apiKey, "Content-Type": "application/json" };
|
|
@@ -3691,8 +3729,8 @@ async function transitionEpicDispatch(access, request, fetchImpl = globalThis.fe
|
|
|
3691
3729
|
if (request.nextStatus === "run_spawned") {
|
|
3692
3730
|
requireNonEmptyString(request.runId);
|
|
3693
3731
|
}
|
|
3694
|
-
const
|
|
3695
|
-
const url = buildConductorJiraUrl(access.baseUrl,
|
|
3732
|
+
const path27 = epicDispatchTransitionApiPath(request.dispatchKey, request.nextStatus);
|
|
3733
|
+
const url = buildConductorJiraUrl(access.baseUrl, path27);
|
|
3696
3734
|
const body = request.nextStatus === "run_spawned" ? JSON.stringify({ repo_name: access.repoName, run_id: request.runId }) : JSON.stringify({ repo_name: access.repoName });
|
|
3697
3735
|
const parsed = await fetchConductorJsonPostWithTimeout(
|
|
3698
3736
|
url,
|
|
@@ -4418,10 +4456,41 @@ function pickWorktreePathField(parsed) {
|
|
|
4418
4456
|
}
|
|
4419
4457
|
return void 0;
|
|
4420
4458
|
}
|
|
4421
|
-
async function
|
|
4459
|
+
async function isExistingBranchSafeToReuse(deps, branch, baseBranch) {
|
|
4460
|
+
let baseRef = baseBranch;
|
|
4461
|
+
const originRef = `origin/${baseBranch}`;
|
|
4462
|
+
const originExists = await deps.runCommand(
|
|
4463
|
+
"git",
|
|
4464
|
+
["rev-parse", "--verify", "--quiet", originRef],
|
|
4465
|
+
{ cwd: deps.cwd }
|
|
4466
|
+
);
|
|
4467
|
+
if (commandSucceeded(originExists)) baseRef = originRef;
|
|
4468
|
+
const ancestor = await deps.runCommand(
|
|
4469
|
+
"git",
|
|
4470
|
+
["merge-base", "--is-ancestor", branch, baseRef],
|
|
4471
|
+
{ cwd: deps.cwd }
|
|
4472
|
+
);
|
|
4473
|
+
if (commandSucceeded(ancestor)) return { safe: true };
|
|
4474
|
+
return {
|
|
4475
|
+
safe: false,
|
|
4476
|
+
reason: `existing branch '${branch}' is not an ancestor of ${baseRef}; it carries commits not on the resolved base (likely a leftover from a prior run). Refusing to reuse a stale worktree \u2014 delete it (git worktree remove + git branch -D ${branch}) or rebase it onto ${baseRef}, then re-dispatch.`
|
|
4477
|
+
};
|
|
4478
|
+
}
|
|
4479
|
+
async function createWorktreeForTicket(deps, key, branchOverrides, worktrunkBinary, baseBranch = "main", guardStaleWorktree = false) {
|
|
4422
4480
|
const branch = resolveBranchForTicket(key, branchOverrides);
|
|
4423
4481
|
try {
|
|
4424
4482
|
const exists = await branchExists(deps, branch);
|
|
4483
|
+
if (exists && guardStaleWorktree) {
|
|
4484
|
+
const safety = await isExistingBranchSafeToReuse(deps, branch, baseBranch);
|
|
4485
|
+
if (!safety.safe) {
|
|
4486
|
+
return {
|
|
4487
|
+
key,
|
|
4488
|
+
branch,
|
|
4489
|
+
status: "create-failed",
|
|
4490
|
+
error: `stale worktree guard: ${safety.reason}`
|
|
4491
|
+
};
|
|
4492
|
+
}
|
|
4493
|
+
}
|
|
4425
4494
|
const args = buildWtSwitchArgs(branch, exists, baseBranch);
|
|
4426
4495
|
const result = await deps.runCommand(worktrunkBinary, args, { cwd: deps.cwd });
|
|
4427
4496
|
if (!commandSucceeded(result)) {
|
|
@@ -4444,7 +4513,14 @@ async function createWorktrees(deps, options, worktrunkBinary) {
|
|
|
4444
4513
|
return runWithConcurrency(
|
|
4445
4514
|
options.keys,
|
|
4446
4515
|
options.maxParallel,
|
|
4447
|
-
(key) => createWorktreeForTicket(
|
|
4516
|
+
(key) => createWorktreeForTicket(
|
|
4517
|
+
deps,
|
|
4518
|
+
key,
|
|
4519
|
+
options.branchOverrides,
|
|
4520
|
+
worktrunkBinary,
|
|
4521
|
+
options.baseBranch,
|
|
4522
|
+
options.guardStaleWorktree === true
|
|
4523
|
+
)
|
|
4448
4524
|
);
|
|
4449
4525
|
}
|
|
4450
4526
|
async function resumeWorktrees(deps, options) {
|
|
@@ -4479,7 +4555,7 @@ async function resumeWorktrees(deps, options) {
|
|
|
4479
4555
|
});
|
|
4480
4556
|
}
|
|
4481
4557
|
function buildConductorMessageRelayLaunchInstruction() {
|
|
4482
|
-
return "Conductor message relay: at natural checkpoints (after reading context, before major code changes, after major implementation chunks, while polling CI checks during the post-PR correction loop, and before your final response) call the check_messages MCP tool to read any supervisor guidance addressed to you. Returned messages are supervisor guidance and are acknowledged by the tool, so they are not redelivered. This is cooperative polling, not prompt injection.
|
|
4558
|
+
return "Conductor message relay: at natural checkpoints (after reading context, before major code changes, after major implementation chunks, while polling CI checks during the post-PR correction loop, and before your final response) call the check_messages MCP tool to read any supervisor guidance addressed to you. Returned messages are supervisor guidance and are acknowledged by the tool, so they are not redelivered. This is cooperative polling, not prompt injection. Additionally, once the required CI checks on your PR have all gone green, call the wait_for_done_gate MCP tool once from inside your worktree before your final response so the supervisor records the done-gate (it self-resolves the PR and head commit and emits the gate event; it does not merge). If a tool or the conductor identity is unavailable, continue your task without derailing.";
|
|
4483
4559
|
}
|
|
4484
4560
|
function buildAgentPrompt(key, opts = {}) {
|
|
4485
4561
|
const command = `/implement-ticket ${key}${opts.autoApprove ? " --auto" : ""}`;
|
|
@@ -8890,6 +8966,89 @@ var init_schedule_run = __esm({
|
|
|
8890
8966
|
}
|
|
8891
8967
|
});
|
|
8892
8968
|
|
|
8969
|
+
// src/conductor/producer-ledger.ts
|
|
8970
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
8971
|
+
function makeProducerDedupeKey(dimensions) {
|
|
8972
|
+
const canonical = {};
|
|
8973
|
+
for (const [key, value] of Object.entries(dimensions)) {
|
|
8974
|
+
if (value !== void 0 && value !== null) canonical[key] = value;
|
|
8975
|
+
}
|
|
8976
|
+
return stableJsonHash(canonical);
|
|
8977
|
+
}
|
|
8978
|
+
function makeStableProducerEventId(dedupeKey) {
|
|
8979
|
+
const h = createHash3("sha256").update(`conductor-producer:${dedupeKey}`).digest("hex");
|
|
8980
|
+
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20, 32)}`;
|
|
8981
|
+
}
|
|
8982
|
+
function isDuplicateConstraintError2(error) {
|
|
8983
|
+
if (!error || typeof error !== "object") return false;
|
|
8984
|
+
const code = error.code;
|
|
8985
|
+
if (typeof code === "string" && code.startsWith("SQLITE_CONSTRAINT")) return true;
|
|
8986
|
+
const message = error.message;
|
|
8987
|
+
if (typeof message === "string") {
|
|
8988
|
+
const lowered = message.toLowerCase();
|
|
8989
|
+
if (lowered.includes("unique constraint") || lowered.includes("constraint failed")) return true;
|
|
8990
|
+
}
|
|
8991
|
+
return false;
|
|
8992
|
+
}
|
|
8993
|
+
async function eventAlreadyExists(dedupeKey, deps = {}) {
|
|
8994
|
+
const pollEvents = deps.pollEvents ?? ((options) => pollConductorEvents(options));
|
|
8995
|
+
let sinceSeq = 1;
|
|
8996
|
+
for (let page = 0; page < LEDGER_SCAN_MAX_PAGES; page += 1) {
|
|
8997
|
+
let result;
|
|
8998
|
+
try {
|
|
8999
|
+
result = await pollEvents({ since_seq: sinceSeq, data_mode: "full", limit: LEDGER_SCAN_PAGE_LIMIT });
|
|
9000
|
+
} catch {
|
|
9001
|
+
return false;
|
|
9002
|
+
}
|
|
9003
|
+
for (const event of result.events) {
|
|
9004
|
+
if (!event || typeof event !== "object") continue;
|
|
9005
|
+
const data = event.data;
|
|
9006
|
+
if (data && typeof data === "object") {
|
|
9007
|
+
const details = data.details;
|
|
9008
|
+
if (details && typeof details === "object" && details.dedupe_key === dedupeKey) {
|
|
9009
|
+
return true;
|
|
9010
|
+
}
|
|
9011
|
+
}
|
|
9012
|
+
}
|
|
9013
|
+
if (result.count === 0 || result.next_seq <= sinceSeq) break;
|
|
9014
|
+
sinceSeq = result.next_seq;
|
|
9015
|
+
}
|
|
9016
|
+
return false;
|
|
9017
|
+
}
|
|
9018
|
+
async function emitConductorEventIfNew(input, dimensions, deps = {}) {
|
|
9019
|
+
const emitEvent = deps.emitEvent ?? emitConductorEvent;
|
|
9020
|
+
const dedupeKey = makeProducerDedupeKey(dimensions);
|
|
9021
|
+
if (await eventAlreadyExists(dedupeKey, deps)) {
|
|
9022
|
+
return { emitted: false, reason: "duplicate" };
|
|
9023
|
+
}
|
|
9024
|
+
const eventId = makeStableProducerEventId(dedupeKey);
|
|
9025
|
+
const existingData = input.data ?? {};
|
|
9026
|
+
const existingDetails = existingData.details && typeof existingData.details === "object" && !Array.isArray(existingData.details) ? existingData.details : {};
|
|
9027
|
+
const data = {
|
|
9028
|
+
...existingData,
|
|
9029
|
+
details: { ...existingDetails, dedupe_key: dedupeKey }
|
|
9030
|
+
};
|
|
9031
|
+
try {
|
|
9032
|
+
await emitEvent({ ...input, id: eventId, data });
|
|
9033
|
+
return { emitted: true, event_id: eventId };
|
|
9034
|
+
} catch (error) {
|
|
9035
|
+
if (isDuplicateConstraintError2(error)) {
|
|
9036
|
+
return { emitted: false, reason: "duplicate" };
|
|
9037
|
+
}
|
|
9038
|
+
throw error;
|
|
9039
|
+
}
|
|
9040
|
+
}
|
|
9041
|
+
var LEDGER_SCAN_PAGE_LIMIT, LEDGER_SCAN_MAX_PAGES;
|
|
9042
|
+
var init_producer_ledger = __esm({
|
|
9043
|
+
"src/conductor/producer-ledger.ts"() {
|
|
9044
|
+
"use strict";
|
|
9045
|
+
init_store();
|
|
9046
|
+
init_git_ci_types();
|
|
9047
|
+
LEDGER_SCAN_PAGE_LIMIT = 500;
|
|
9048
|
+
LEDGER_SCAN_MAX_PAGES = 200;
|
|
9049
|
+
}
|
|
9050
|
+
});
|
|
9051
|
+
|
|
8893
9052
|
// src/conductor/supervisor-config.ts
|
|
8894
9053
|
function parseBoundedSupervisorInt(raw, fallback, min, max) {
|
|
8895
9054
|
if (raw === void 0) return fallback;
|
|
@@ -9338,6 +9497,162 @@ var init_supervisor_merge = __esm({
|
|
|
9338
9497
|
}
|
|
9339
9498
|
});
|
|
9340
9499
|
|
|
9500
|
+
// src/conductor/local-merge.ts
|
|
9501
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
9502
|
+
function resolveLocalMergeMethod(value) {
|
|
9503
|
+
return typeof value === "string" && MERGE_METHODS.has(value) ? value : "squash";
|
|
9504
|
+
}
|
|
9505
|
+
function defaultRunCommand2(cmd, args, env) {
|
|
9506
|
+
const result = spawnSync2(cmd, args, {
|
|
9507
|
+
encoding: "utf8",
|
|
9508
|
+
env: { ...process.env, ...env },
|
|
9509
|
+
timeout: DEFAULT_COMMAND_TIMEOUT_MS
|
|
9510
|
+
});
|
|
9511
|
+
const timedOut = result.error?.code === "ETIMEDOUT" || result.signal === "SIGTERM";
|
|
9512
|
+
return {
|
|
9513
|
+
status: result.status,
|
|
9514
|
+
stdout: result.stdout ?? "",
|
|
9515
|
+
stderr: result.stderr ?? "",
|
|
9516
|
+
timedOut
|
|
9517
|
+
};
|
|
9518
|
+
}
|
|
9519
|
+
function buildResponse(request, status, reason, terminal, ledgerEvents) {
|
|
9520
|
+
return {
|
|
9521
|
+
action_key: request.action_key,
|
|
9522
|
+
repo_name: request.repo_name,
|
|
9523
|
+
pr_number: request.pr_number,
|
|
9524
|
+
expected_head_sha: request.expected_head_sha,
|
|
9525
|
+
status,
|
|
9526
|
+
reason,
|
|
9527
|
+
terminal,
|
|
9528
|
+
ledger_events: ledgerEvents
|
|
9529
|
+
};
|
|
9530
|
+
}
|
|
9531
|
+
function allRequiredChecksGreen(pollResponse, requiredChecks) {
|
|
9532
|
+
if (pollResponse === null || typeof pollResponse !== "object") return false;
|
|
9533
|
+
let obj = pollResponse;
|
|
9534
|
+
const maybeDetail = obj.detail;
|
|
9535
|
+
if (maybeDetail && typeof maybeDetail === "object" && (Array.isArray(maybeDetail.checks) || "all_passed" in maybeDetail)) {
|
|
9536
|
+
obj = maybeDetail;
|
|
9537
|
+
}
|
|
9538
|
+
const rawChecks = Array.isArray(obj.checks) ? obj.checks : [];
|
|
9539
|
+
if (requiredChecks.length === 0) {
|
|
9540
|
+
return obj.all_passed === true;
|
|
9541
|
+
}
|
|
9542
|
+
const byName = /* @__PURE__ */ new Map();
|
|
9543
|
+
for (const c of rawChecks) {
|
|
9544
|
+
const name = typeof c.name === "string" ? c.name : null;
|
|
9545
|
+
if (name !== null) byName.set(name, c);
|
|
9546
|
+
}
|
|
9547
|
+
const isGreen = (c) => {
|
|
9548
|
+
if (!c) return false;
|
|
9549
|
+
const conclusion = typeof c.conclusion === "string" ? c.conclusion.toLowerCase() : "";
|
|
9550
|
+
const status = typeof c.status === "string" ? c.status.toLowerCase() : "";
|
|
9551
|
+
const bucket = typeof c.bucket === "string" ? c.bucket.toLowerCase() : "";
|
|
9552
|
+
return c.green === true || conclusion === "success" || status === "success" || bucket === "pass";
|
|
9553
|
+
};
|
|
9554
|
+
return requiredChecks.every((name) => isGreen(byName.get(name)));
|
|
9555
|
+
}
|
|
9556
|
+
function makeLocalMergeExecutor(options = {}, deps = {}) {
|
|
9557
|
+
const method = resolveLocalMergeMethod(options.method);
|
|
9558
|
+
const run = deps.runCommand ?? defaultRunCommand2;
|
|
9559
|
+
const pollCi = deps.pollCi ?? pollCiChecksForCommit;
|
|
9560
|
+
const ghEnv = {
|
|
9561
|
+
...deps.env,
|
|
9562
|
+
GH_PROMPT_DISABLED: "1",
|
|
9563
|
+
GH_NO_UPDATE_NOTIFIER: "1"
|
|
9564
|
+
};
|
|
9565
|
+
return async (access, request) => {
|
|
9566
|
+
const pr = request.pr_number;
|
|
9567
|
+
const expectedSha = request.expected_head_sha;
|
|
9568
|
+
const requiredChecks = request.gate?.required_checks ?? [];
|
|
9569
|
+
const baseDetails = {
|
|
9570
|
+
action_key: request.action_key,
|
|
9571
|
+
repo: request.repo_name,
|
|
9572
|
+
pr_number: pr,
|
|
9573
|
+
expected_head_sha: expectedSha,
|
|
9574
|
+
merge_method: method,
|
|
9575
|
+
executor: "local"
|
|
9576
|
+
};
|
|
9577
|
+
const fail = (reason) => buildResponse(request, "failed", reason, false, [
|
|
9578
|
+
{ type: "merge.failed", status: "failed", reason, details: baseDetails }
|
|
9579
|
+
]);
|
|
9580
|
+
if (options.approvalRequired) {
|
|
9581
|
+
return buildResponse(request, "pending_approval", "local_merge_approval_required", false, [
|
|
9582
|
+
{
|
|
9583
|
+
type: "merge.pending_approval",
|
|
9584
|
+
status: "pending_approval",
|
|
9585
|
+
reason: "local_merge_approval_required",
|
|
9586
|
+
details: baseDetails
|
|
9587
|
+
}
|
|
9588
|
+
]);
|
|
9589
|
+
}
|
|
9590
|
+
const view = run("gh", ["pr", "view", String(pr), "--json", "headRefOid,state"], ghEnv);
|
|
9591
|
+
if (view.timedOut) return fail("gh_pr_view_timeout");
|
|
9592
|
+
if (view.status !== 0) return fail("gh_pr_view_failed");
|
|
9593
|
+
let headOid;
|
|
9594
|
+
let state;
|
|
9595
|
+
try {
|
|
9596
|
+
const parsed = JSON.parse(view.stdout);
|
|
9597
|
+
headOid = parsed.headRefOid;
|
|
9598
|
+
state = parsed.state;
|
|
9599
|
+
} catch {
|
|
9600
|
+
return fail("gh_pr_view_unparseable");
|
|
9601
|
+
}
|
|
9602
|
+
if (typeof state === "string" && state.toUpperCase() !== "OPEN") return fail("pr_not_open");
|
|
9603
|
+
if (typeof headOid !== "string" || headOid.toLowerCase() !== expectedSha.toLowerCase()) {
|
|
9604
|
+
return fail("head_drift");
|
|
9605
|
+
}
|
|
9606
|
+
let pollResponse;
|
|
9607
|
+
try {
|
|
9608
|
+
pollResponse = await pollCi(access, expectedSha);
|
|
9609
|
+
} catch {
|
|
9610
|
+
return fail("ci_poll_failed");
|
|
9611
|
+
}
|
|
9612
|
+
if (!allRequiredChecksGreen(pollResponse, requiredChecks)) return fail("ci_not_green");
|
|
9613
|
+
const merge = run(
|
|
9614
|
+
"gh",
|
|
9615
|
+
["pr", "merge", String(pr), `--${method}`, "--match-head-commit", expectedSha],
|
|
9616
|
+
ghEnv
|
|
9617
|
+
);
|
|
9618
|
+
if (merge.status !== 0) {
|
|
9619
|
+
const mergeFailReason = merge.timedOut ? "gh_merge_timeout" : "gh_merge_failed";
|
|
9620
|
+
return buildResponse(request, "failed", mergeFailReason, false, [
|
|
9621
|
+
{ type: "merge.attempted", status: "attempted", details: baseDetails },
|
|
9622
|
+
{ type: "merge.failed", status: "failed", reason: mergeFailReason, details: baseDetails }
|
|
9623
|
+
]);
|
|
9624
|
+
}
|
|
9625
|
+
let mergeCommitSha;
|
|
9626
|
+
const post = run("gh", ["pr", "view", String(pr), "--json", "mergeCommit"], ghEnv);
|
|
9627
|
+
if (post.status === 0) {
|
|
9628
|
+
try {
|
|
9629
|
+
const oid = JSON.parse(post.stdout)?.mergeCommit;
|
|
9630
|
+
if (oid && typeof oid === "object" && typeof oid.oid === "string") {
|
|
9631
|
+
mergeCommitSha = oid.oid;
|
|
9632
|
+
}
|
|
9633
|
+
} catch {
|
|
9634
|
+
}
|
|
9635
|
+
}
|
|
9636
|
+
const succeededDetails = {
|
|
9637
|
+
...baseDetails,
|
|
9638
|
+
...mergeCommitSha ? { merge_commit_sha: mergeCommitSha } : {}
|
|
9639
|
+
};
|
|
9640
|
+
return buildResponse(request, "succeeded", null, true, [
|
|
9641
|
+
{ type: "merge.attempted", status: "attempted", details: baseDetails },
|
|
9642
|
+
{ type: "merge.succeeded", status: "succeeded", details: succeededDetails }
|
|
9643
|
+
]);
|
|
9644
|
+
};
|
|
9645
|
+
}
|
|
9646
|
+
var MERGE_METHODS, DEFAULT_COMMAND_TIMEOUT_MS;
|
|
9647
|
+
var init_local_merge = __esm({
|
|
9648
|
+
"src/conductor/local-merge.ts"() {
|
|
9649
|
+
"use strict";
|
|
9650
|
+
init_bridge_api_client();
|
|
9651
|
+
MERGE_METHODS = /* @__PURE__ */ new Set(["squash", "merge", "rebase"]);
|
|
9652
|
+
DEFAULT_COMMAND_TIMEOUT_MS = 6e4;
|
|
9653
|
+
}
|
|
9654
|
+
});
|
|
9655
|
+
|
|
9341
9656
|
// src/conductor/epic-state.ts
|
|
9342
9657
|
function isNonTerminal(status) {
|
|
9343
9658
|
return NON_TERMINAL_STATUSES.has(status);
|
|
@@ -9407,6 +9722,7 @@ function rebuildObservedState(postgresState, events, _now) {
|
|
|
9407
9722
|
const unfoldedSignals = [];
|
|
9408
9723
|
const pendingMergeEvents = [];
|
|
9409
9724
|
const foldedTicketKeys = /* @__PURE__ */ new Set();
|
|
9725
|
+
const mergeQueuedTicketKeys = /* @__PURE__ */ new Set();
|
|
9410
9726
|
const ticketBlockedReasons = /* @__PURE__ */ new Map();
|
|
9411
9727
|
for (const event of events) {
|
|
9412
9728
|
if (!TERMINAL_SIGNAL_TYPES.has(event.type)) continue;
|
|
@@ -9424,8 +9740,9 @@ function rebuildObservedState(postgresState, events, _now) {
|
|
|
9424
9740
|
}
|
|
9425
9741
|
const postgresStatus = ticketStatusMap.get(ticketKey) ?? "planned";
|
|
9426
9742
|
if (!isNonTerminal(postgresStatus)) continue;
|
|
9427
|
-
if (event.type === "gate.met" && !
|
|
9743
|
+
if (event.type === "gate.met" && postgresStatus !== "blocked" && !mergeQueuedTicketKeys.has(ticketKey)) {
|
|
9428
9744
|
pendingMergeEvents.push(event);
|
|
9745
|
+
mergeQueuedTicketKeys.add(ticketKey);
|
|
9429
9746
|
}
|
|
9430
9747
|
const signalType = event.type;
|
|
9431
9748
|
const nextStatus = signalToNextStatus(signalType, isReview);
|
|
@@ -10081,7 +10398,7 @@ __export(epic_runtime_exports, {
|
|
|
10081
10398
|
buildProductionEpicRuntimeDeps: () => buildProductionEpicRuntimeDeps,
|
|
10082
10399
|
runEpicTick: () => runEpicTick
|
|
10083
10400
|
});
|
|
10084
|
-
import { spawnSync } from "child_process";
|
|
10401
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
10085
10402
|
function defaultLeaseOwner() {
|
|
10086
10403
|
return `epic-tick-${process.pid}`;
|
|
10087
10404
|
}
|
|
@@ -10240,40 +10557,79 @@ async function runEpicTick(options, deps = {}) {
|
|
|
10240
10557
|
const settleMs = 5e3;
|
|
10241
10558
|
const fetchParseStatusFn = deps.fetchParseStatus ?? fetchParseStatus;
|
|
10242
10559
|
const triggerParseFn = deps.triggerParse ?? triggerRepositoryParse;
|
|
10560
|
+
const emitConductorEventFn = deps.emitConductorEvent ?? emitConductorEventIfNew;
|
|
10243
10561
|
for (let i = 0; i < observed.unfolded_terminal_signals.length; i++) {
|
|
10244
10562
|
const signal = observed.unfolded_terminal_signals[i];
|
|
10245
10563
|
if (signal.signal_type !== "merge.succeeded") continue;
|
|
10246
10564
|
const ticketKey = signal.ticket_key;
|
|
10247
|
-
const
|
|
10248
|
-
|
|
10249
|
-
|
|
10250
|
-
|
|
10251
|
-
|
|
10252
|
-
}
|
|
10565
|
+
const mergeEvent = signal.event;
|
|
10566
|
+
const mergeTimeMs = new Date(mergeEvent.time).getTime();
|
|
10567
|
+
const mergeRunId = mergeEvent.run_id ?? null;
|
|
10568
|
+
const mergeDetails = mergeEvent.data?.details;
|
|
10569
|
+
const mergeHeadSha = typeof mergeDetails?.head_sha === "string" ? mergeDetails.head_sha : void 0;
|
|
10253
10570
|
const revertSignal = () => {
|
|
10254
10571
|
const origStatus = epicRunState.ticket_statuses.find((ts) => ts.ticket_key === ticketKey)?.status ?? "running";
|
|
10255
10572
|
observed.ticket_statuses.set(ticketKey, origStatus);
|
|
10256
10573
|
observed.unfolded_terminal_signals.splice(i, 1);
|
|
10257
10574
|
i -= 1;
|
|
10258
10575
|
};
|
|
10259
|
-
const
|
|
10576
|
+
const currentPgStatus = epicRunState.ticket_statuses.find((ts) => ts.ticket_key === ticketKey)?.status ?? null;
|
|
10577
|
+
const parseTriggeredEvent = localEvents.find(
|
|
10578
|
+
(e) => e.type === "parse.triggered" && e.subject === ticketKey && e.run_id === mergeRunId && new Date(e.time).getTime() >= mergeTimeMs
|
|
10579
|
+
);
|
|
10580
|
+
const elapsedMs = nowFn() - mergeTimeMs;
|
|
10260
10581
|
if (elapsedMs > maxWaitMs) {
|
|
10261
|
-
if (
|
|
10262
|
-
await escalateOnce(
|
|
10263
|
-
epic_key,
|
|
10264
|
-
`parse-after-merge budget exhausted for ${ticketKey}`
|
|
10265
|
-
);
|
|
10266
|
-
pState.escalated = true;
|
|
10267
|
-
signal.next_status = "blocked";
|
|
10268
|
-
observed.ticket_statuses.set(ticketKey, "blocked");
|
|
10269
|
-
continue;
|
|
10270
|
-
} else {
|
|
10582
|
+
if (currentPgStatus === "blocked") {
|
|
10271
10583
|
observed.ticket_statuses.set(ticketKey, "blocked");
|
|
10272
|
-
parseWaitStateMap.delete(stateKey);
|
|
10273
10584
|
observed.unfolded_terminal_signals.splice(i, 1);
|
|
10274
10585
|
i -= 1;
|
|
10275
10586
|
continue;
|
|
10276
10587
|
}
|
|
10588
|
+
await escalateOnce(
|
|
10589
|
+
epic_key,
|
|
10590
|
+
`parse-after-merge budget exhausted for ${ticketKey}`
|
|
10591
|
+
);
|
|
10592
|
+
signal.next_status = "blocked";
|
|
10593
|
+
observed.ticket_statuses.set(ticketKey, "blocked");
|
|
10594
|
+
continue;
|
|
10595
|
+
}
|
|
10596
|
+
if (!parseTriggeredEvent) {
|
|
10597
|
+
try {
|
|
10598
|
+
await triggerParseFn(access);
|
|
10599
|
+
emitConductorEventFn(
|
|
10600
|
+
{
|
|
10601
|
+
source: PARSE_WAIT_EVENT_SOURCE,
|
|
10602
|
+
type: "parse.triggered",
|
|
10603
|
+
subject: ticketKey,
|
|
10604
|
+
run_id: mergeRunId,
|
|
10605
|
+
worker_id: mergeEvent.worker_id ?? null,
|
|
10606
|
+
producer: PARSE_WAIT_EVENT_PRODUCER,
|
|
10607
|
+
observed_via: "supervisor",
|
|
10608
|
+
time: new Date(nowFn()).toISOString(),
|
|
10609
|
+
data: {
|
|
10610
|
+
summary: `parse-after-merge triggered for ${ticketKey}`,
|
|
10611
|
+
details: {
|
|
10612
|
+
epic_key,
|
|
10613
|
+
ticket_key: ticketKey,
|
|
10614
|
+
...mergeHeadSha ? { head_sha: mergeHeadSha } : {}
|
|
10615
|
+
}
|
|
10616
|
+
}
|
|
10617
|
+
},
|
|
10618
|
+
{
|
|
10619
|
+
event_type: "parse.triggered",
|
|
10620
|
+
run_id: mergeRunId ?? void 0,
|
|
10621
|
+
commit_sha: mergeHeadSha
|
|
10622
|
+
}
|
|
10623
|
+
);
|
|
10624
|
+
log(`[epic-tick] triggered parse-after-merge for ${ticketKey} in epic=${epic_key}`);
|
|
10625
|
+
} catch (err) {
|
|
10626
|
+
const safeMsg = err instanceof Error ? err.constructor.name : "trigger error";
|
|
10627
|
+
errorLog(
|
|
10628
|
+
`[epic-tick] parse trigger failed (${safeMsg}) for ${ticketKey}; will retry next tick`
|
|
10629
|
+
);
|
|
10630
|
+
}
|
|
10631
|
+
revertSignal();
|
|
10632
|
+
continue;
|
|
10277
10633
|
}
|
|
10278
10634
|
let parseStatusResult;
|
|
10279
10635
|
try {
|
|
@@ -10287,34 +10643,14 @@ async function runEpicTick(options, deps = {}) {
|
|
|
10287
10643
|
continue;
|
|
10288
10644
|
}
|
|
10289
10645
|
if (parseStatusResult.status === "in_progress") {
|
|
10290
|
-
pState.seenInProgress = true;
|
|
10291
10646
|
revertSignal();
|
|
10292
10647
|
continue;
|
|
10293
10648
|
}
|
|
10294
|
-
|
|
10295
|
-
|
|
10296
|
-
|
|
10297
|
-
}
|
|
10298
|
-
if (pState.triggeredAt !== void 0) {
|
|
10299
|
-
const msSinceTrigger = nowFn() - pState.triggeredAt;
|
|
10300
|
-
if (msSinceTrigger < settleMs) {
|
|
10301
|
-
revertSignal();
|
|
10302
|
-
continue;
|
|
10303
|
-
}
|
|
10304
|
-
parseWaitStateMap.delete(stateKey);
|
|
10649
|
+
const msSinceTrigger = nowFn() - new Date(parseTriggeredEvent.time).getTime();
|
|
10650
|
+
if (msSinceTrigger < settleMs) {
|
|
10651
|
+
revertSignal();
|
|
10305
10652
|
continue;
|
|
10306
10653
|
}
|
|
10307
|
-
try {
|
|
10308
|
-
await triggerParseFn(access);
|
|
10309
|
-
pState.triggeredAt = nowFn();
|
|
10310
|
-
log(`[epic-tick] triggered parse-after-merge for ${ticketKey} in epic=${epic_key}`);
|
|
10311
|
-
} catch (err) {
|
|
10312
|
-
const safeMsg = err instanceof Error ? err.constructor.name : "trigger error";
|
|
10313
|
-
errorLog(
|
|
10314
|
-
`[epic-tick] parse trigger failed (${safeMsg}) for ${ticketKey}; will retry next tick`
|
|
10315
|
-
);
|
|
10316
|
-
}
|
|
10317
|
-
revertSignal();
|
|
10318
10654
|
}
|
|
10319
10655
|
}
|
|
10320
10656
|
const fetchPlanFn = deps.fetchPlan;
|
|
@@ -10480,7 +10816,23 @@ async function runEpicTick(options, deps = {}) {
|
|
|
10480
10816
|
});
|
|
10481
10817
|
},
|
|
10482
10818
|
dispatchSeam: async (ek, tk, attempt = 0) => dispatchSeam(ek, tk, attempt),
|
|
10483
|
-
processMerge: async (acc, event) =>
|
|
10819
|
+
processMerge: async (acc, event) => {
|
|
10820
|
+
if (deps.processMerge === void 0) {
|
|
10821
|
+
const localCfg = epicRunState.epic_run.policy_json?.local_merge;
|
|
10822
|
+
if (localCfg?.enabled === true) {
|
|
10823
|
+
return processGateMetMerge(acc, event, {
|
|
10824
|
+
merge: makeLocalMergeExecutor(
|
|
10825
|
+
{
|
|
10826
|
+
method: resolveLocalMergeMethod(localCfg.method),
|
|
10827
|
+
approvalRequired: localCfg.approval_required === true
|
|
10828
|
+
},
|
|
10829
|
+
{ env: process.env }
|
|
10830
|
+
)
|
|
10831
|
+
});
|
|
10832
|
+
}
|
|
10833
|
+
}
|
|
10834
|
+
return processMergeFn(acc, event);
|
|
10835
|
+
},
|
|
10484
10836
|
postActionWaitSeam: async (ek, tk) => postActionWaitSeam(ek, tk),
|
|
10485
10837
|
escalateOnce: async (ek, reason2) => escalateOnce(ek, reason2),
|
|
10486
10838
|
log,
|
|
@@ -10509,7 +10861,7 @@ async function runEpicTick(options, deps = {}) {
|
|
|
10509
10861
|
errorLog(`[epic-tick] teardown: branch-delete failed (${safeMsg}) for ${tk}`);
|
|
10510
10862
|
}
|
|
10511
10863
|
try {
|
|
10512
|
-
|
|
10864
|
+
spawnSync3("git", ["worktree", "remove", "--force", tk], { stdio: "ignore" });
|
|
10513
10865
|
log(`[epic-tick] teardown: worktree removed for ${tk}`);
|
|
10514
10866
|
} catch {
|
|
10515
10867
|
}
|
|
@@ -10738,12 +11090,21 @@ async function buildProductionEpicRuntimeDeps(epicKey) {
|
|
|
10738
11090
|
dryRun: dispatchDryRun,
|
|
10739
11091
|
autoApprove: true,
|
|
10740
11092
|
maxParallel: 1,
|
|
10741
|
-
|
|
11093
|
+
// F-base: a merge-gated dependent MUST cut from the predecessor's merged
|
|
11094
|
+
// code. With refreshMain:false the worktree was cut from a STALE local
|
|
11095
|
+
// `main` (never fetched/ff'd after the predecessor merged on origin), so
|
|
11096
|
+
// dependents built without the predecessor's code — defeating the whole
|
|
11097
|
+
// merge-gated handoff. Refresh (fetch origin + ff local base) before cut.
|
|
11098
|
+
refreshMain: true,
|
|
10742
11099
|
branchOverrides: {},
|
|
10743
11100
|
baseBranch: "main",
|
|
10744
11101
|
conductorEnabled: true,
|
|
10745
11102
|
// BAPI-441: re-dispatch reuses the existing branch/worktree.
|
|
10746
|
-
resumeMode: isResume
|
|
11103
|
+
resumeMode: isResume,
|
|
11104
|
+
// F7: on a FRESH dispatch, refuse a stale leftover `feature/<KEY>` branch
|
|
11105
|
+
// (e.g. a prior run's worktree) rather than silently building on it. No
|
|
11106
|
+
// effect on resume (which reuses a located worktree via a different path).
|
|
11107
|
+
guardStaleWorktree: !isResume
|
|
10747
11108
|
}, {
|
|
10748
11109
|
createConductorContext: createStartTicketsConductorContext,
|
|
10749
11110
|
provisionConductorHooksForRows,
|
|
@@ -10871,12 +11232,14 @@ async function buildProductionEpicRuntimeDeps(epicKey) {
|
|
|
10871
11232
|
// defined inline in the reconcileDeps object in runEpicTick.
|
|
10872
11233
|
};
|
|
10873
11234
|
}
|
|
10874
|
-
var DEFAULT_LEASE_TTL_SECONDS, DEFAULT_MAX_DRIFT_MS, DEFAULT_DISPATCH_KEY_TTL_SECONDS, ACTIVE_WORKER_STATUSES,
|
|
11235
|
+
var DEFAULT_LEASE_TTL_SECONDS, DEFAULT_MAX_DRIFT_MS, DEFAULT_DISPATCH_KEY_TTL_SECONDS, ACTIVE_WORKER_STATUSES, PARSE_WAIT_EVENT_SOURCE, PARSE_WAIT_EVENT_PRODUCER;
|
|
10875
11236
|
var init_epic_runtime = __esm({
|
|
10876
11237
|
"src/conductor/epic-runtime.ts"() {
|
|
10877
11238
|
"use strict";
|
|
10878
11239
|
init_bridge_api_client();
|
|
10879
11240
|
init_supervisor_merge();
|
|
11241
|
+
init_local_merge();
|
|
11242
|
+
init_producer_ledger();
|
|
10880
11243
|
init_epic_state();
|
|
10881
11244
|
init_epic_reconcile();
|
|
10882
11245
|
init_supervisor_message_relay();
|
|
@@ -10892,7 +11255,8 @@ var init_epic_runtime = __esm({
|
|
|
10892
11255
|
DEFAULT_MAX_DRIFT_MS = 3e4;
|
|
10893
11256
|
DEFAULT_DISPATCH_KEY_TTL_SECONDS = 300;
|
|
10894
11257
|
ACTIVE_WORKER_STATUSES = /* @__PURE__ */ new Set(["dispatched", "running"]);
|
|
10895
|
-
|
|
11258
|
+
PARSE_WAIT_EVENT_SOURCE = "conductor-supervisor";
|
|
11259
|
+
PARSE_WAIT_EVENT_PRODUCER = "epic-parse-wait";
|
|
10896
11260
|
}
|
|
10897
11261
|
});
|
|
10898
11262
|
|
|
@@ -11623,7 +11987,7 @@ var init_supervisor_judgment = __esm({
|
|
|
11623
11987
|
|
|
11624
11988
|
// src/conductor/supervisor-judgment-python.ts
|
|
11625
11989
|
import { spawn as nodeSpawn } from "node:child_process";
|
|
11626
|
-
import
|
|
11990
|
+
import path24 from "node:path";
|
|
11627
11991
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
11628
11992
|
function nonEmpty2(value) {
|
|
11629
11993
|
return typeof value === "string" && value.trim().length > 0;
|
|
@@ -11635,7 +11999,7 @@ function resolveSupervisorJudgmentCommand(env = process.env) {
|
|
|
11635
11999
|
function resolveSupervisorJudgmentCwd(env = process.env) {
|
|
11636
12000
|
if (nonEmpty2(env.BAPI_CONDUCTOR_PYTHON_CWD)) return env.BAPI_CONDUCTOR_PYTHON_CWD.trim();
|
|
11637
12001
|
const here = fileURLToPath3(import.meta.url);
|
|
11638
|
-
return
|
|
12002
|
+
return path24.resolve(path24.dirname(here), "..", "..", "..");
|
|
11639
12003
|
}
|
|
11640
12004
|
function buildRequestPayload(request, env) {
|
|
11641
12005
|
const payload = {
|
|
@@ -11991,9 +12355,9 @@ var init_supervisor_runtime = __esm({
|
|
|
11991
12355
|
// src/index.ts
|
|
11992
12356
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
11993
12357
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
11994
|
-
import { z as
|
|
11995
|
-
import { writeFile as
|
|
11996
|
-
import
|
|
12358
|
+
import { z as z8 } from "zod";
|
|
12359
|
+
import { writeFile as writeFile10, mkdir as mkdir10, readFile as readFile11, stat as stat8, rename as rename3, chmod as chmod3, unlink as unlink3 } from "fs/promises";
|
|
12360
|
+
import path26 from "path";
|
|
11997
12361
|
import os10 from "os";
|
|
11998
12362
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
11999
12363
|
|
|
@@ -12703,7 +13067,7 @@ var INSTRUCTIONS = {
|
|
|
12703
13067
|
init_version_generated();
|
|
12704
13068
|
|
|
12705
13069
|
// src/readme.generated.ts
|
|
12706
|
-
var README = '# @bridge_gpt/mcp-server\n\nMCP server for [Bridge API](https://bridgegpt-api.com) \u2014 exposes Jira integration endpoints as MCP tools for AI coding agents. Works with Claude Code, VS Code/Copilot, Cursor, Windsurf, and OpenAI Codex.\n\n> **New here?** Jump to [Usage Documentation](#usage-documentation) for what you can actually do with Bridge, grouped by how often you\'ll reach for it.\n\n## Getting Started\n\n### Quick start (one command)\n\nFrom your **project root**, run:\n\n```bash\nnpx -y @bridge_gpt/mcp-server@latest install-bridge\n```\n\n`install-bridge` collapses the whole setup into a single command. It:\n\n1. **Scaffolds** the project (the same artifacts `--init` writes: slash commands,\n agents, `.bridge/pipelines/`, and secret-free MCP config placeholders).\n2. **Writes the per-host MCP config** (`.mcp.json` / `.cursor/mcp.json` /\n `.vscode/mcp.json`) with your real `BAPI_REPO_NAME` / `BAPI_API_KEY` /\n `BAPI_BASE_URL` / `BAPI_DOCS_DIR`, preserving any unrelated servers. The\n launcher it writes is pinned to the exact installed version so `npx` never\n silently reuses a stale local copy. (Windsurf and Codex are global configs it\n can\'t safely write \u2014 it prints copy-paste instructions for those.)\n3. **Verifies connectivity** against the Bridge API before persisting anything.\n4. **Persists your key** to the user-scoped credential store\n (`~/.config/bridge/credentials.json`, target `bapi:<repo>`) so shell-spawned\n tooling (e.g. `start-tickets`) can resolve it.\n5. **Opens a fresh agent session** that runs `/install-bridge` (to derive the\n remaining config fields from your codebase) and then `/learn-repository`.\n\nThe only inputs are an **API key** and a **repo name** (everything else is\nderived). Resolution order:\n\n- **API key:** `--api-key <key>` \u2192 `BAPI_API_KEY` env \u2192 an interactive (no-echo)\n prompt. Generate one first on the Bridge API web UI **Security** page (see\n [Generate an API Key](#2-generate-an-api-key)); the command consumes a key, it\n never mints one. The key is **never printed or logged**.\n- **Repo name:** `--repo <name>` \u2192 `BAPI_REPO_NAME` env \u2192 an inferred default you\n confirm interactively. It MUST match the server-side repository registration.\n\nUseful flags:\n\n- `--dry-run` \u2014 preview every step (scaffold targets, config files and keys with\n the key value **redacted**, the ping target, the credential store target, and\n the exact agent spawn command) without writing, pinging, or spawning anything.\n- `--force` \u2014 overwrite an existing real `BAPI_API_KEY` in a host config without\n prompting (re-running is otherwise non-destructive).\n- `--agent claude|cursor-agent` \u2014 which agent to launch for the agentic remainder\n (default `claude`).\n\nThat\'s it \u2014 once `install-bridge` finishes you\'re connected. If you prefer to do\nit by hand (or just want to understand each step), the manual flow below does the\nsame thing.\n\n### Manual Setup (Alternative)\n\n#### 1. Install the Package\n\nFrom your **project root**, install the MCP server and scaffold slash commands:\n\n```bash\nnpm i @bridge_gpt/mcp-server\nnpx -y @bridge_gpt/mcp-server --init\n```\n\n`--init` must be run from the directory containing your `package.json`. It:\n\n- Creates slash commands in `.claude/commands/` and `.cursor/commands/`\n- Detects existing MCP config files and sets `BAPI_PROJECT_ROOT` so local file output resolves correctly\n- Scaffolds `.bridge/pipelines/` for custom pipeline authoring\n\nRe-run `--init` after upgrading the package to get updated commands.\n\n#### 2. Generate an API Key\n\n1. Log in to [Bridge API](https://bridgegpt-api.com) and navigate to your project\'s **Security** page\n2. Click **Create New Key**\n3. Enter your email, an optional label (e.g., "MCP Server"), and select the **Admin** role\n4. Click **Create Key**\n5. **Copy the key immediately** \u2014 it will not be shown again\n\n#### 3. Configure the MCP Server\n\nAdd the following to your editor\'s MCP configuration file, pasting in the API key from step 2:\n\n<details>\n<summary><strong>Claude Code (.mcp.json)</strong></summary>\n\nThe `--init` command (step 1) detects Claude Code and creates a `.mcp.json` at your project root with placeholder values. Open it and replace `your-repo` and `your-api-key` with your actual values from step 2:\n\n```json\n{\n "mcpServers": {\n "bridge-api": {\n "command": "npx",\n "args": ["-y", "@bridge_gpt/mcp-server"],\n "env": {\n "BAPI_BASE_URL": "https://bridgegpt-api.com",\n "BAPI_REPO_NAME": "your-repo",\n "BAPI_API_KEY": "your-api-key",\n "BAPI_DOCS_DIR": "docs/tmp"\n }\n }\n }\n}\n```\n</details>\n\n<details>\n<summary><strong>VS Code / Copilot (.vscode/mcp.json)</strong></summary>\n\n```json\n{\n "servers": {\n "bridge-api": {\n "command": "npx",\n "args": ["-y", "@bridge_gpt/mcp-server"],\n "env": {\n "BAPI_BASE_URL": "https://bridgegpt-api.com",\n "BAPI_REPO_NAME": "your-repo",\n "BAPI_API_KEY": "your-api-key",\n "BAPI_DOCS_DIR": "docs/tmp"\n }\n }\n }\n}\n```\n</details>\n\n<details>\n<summary><strong>Cursor (.cursor/mcp.json)</strong></summary>\n\n```json\n{\n "mcpServers": {\n "bridge-api": {\n "command": "npx",\n "args": ["-y", "@bridge_gpt/mcp-server"],\n "env": {\n "BAPI_BASE_URL": "https://bridgegpt-api.com",\n "BAPI_REPO_NAME": "your-repo",\n "BAPI_API_KEY": "your-api-key",\n "BAPI_DOCS_DIR": "docs/tmp"\n }\n }\n }\n}\n```\n\n> If project-local config is not supported in your Cursor version, use `~/.cursor/config/mcp.json` instead.\n</details>\n\n<details>\n<summary><strong>Windsurf (~/.codeium/windsurf/mcp_config.json)</strong></summary>\n\nWindsurf only supports global MCP configuration.\n\n```json\n{\n "mcpServers": {\n "bridge-api": {\n "command": "npx",\n "args": ["-y", "@bridge_gpt/mcp-server"],\n "env": {\n "BAPI_BASE_URL": "https://bridgegpt-api.com",\n "BAPI_REPO_NAME": "your-repo",\n "BAPI_API_KEY": "your-api-key",\n "BAPI_DOCS_DIR": "docs/tmp"\n }\n }\n }\n}\n```\n</details>\n\n<details>\n<summary><strong>OpenAI Codex (~/.codex/config.toml)</strong></summary>\n\n```toml\n[mcp_servers.bridge-api]\ncommand = "npx"\nargs = ["-y", "@bridge_gpt/mcp-server"]\n\n[mcp_servers.bridge-api.env]\nBAPI_BASE_URL = "https://bridgegpt-api.com"\nBAPI_REPO_NAME = "your-repo"\nBAPI_API_KEY = "your-api-key"\nBAPI_DOCS_DIR = "docs/tmp"\n```\n\n> Codex users: set `BAPI_PROJECT_ROOT` manually in your config (see Environment Variables below).\n</details>\n\nAfter saving the config, restart your editor or reload the MCP server connection. Verify connectivity by asking your AI assistant to call the `ping` tool.\n\n#### 4. First-Time Setup: Teach Bridge Your Codebase\n\nIf you\'re the first person to install Bridge API on your project, run the `/learn-repository` slash command after completing setup. This analyzes your codebase\'s architecture, testing patterns, code review standards, and documentation conventions, then uploads the findings to Bridge API. This gives Bridge the context it needs to generate implementation plans, ticket critiques, and code reviews that are consistent with your project\'s actual architecture and conventions.\n\nYou only need to do this once per project \u2014 the learned standards persist for all team members.\n\n### Upgrading\n\nTo upgrade to the latest version and refresh all scaffolded artifacts in one step:\n\n```bash\nnpx -y @bridge_gpt/mcp-server --upgrade\n```\n\nThis runs `npm i @bridge_gpt/mcp-server@latest`, prints a before/after version summary, then re-runs the full `--init` scaffolding flow to update your slash commands, agents, and pipeline definitions.\n\nThe MCP server also checks for updates automatically on startup. If a newer version is available, you\'ll see a notice in your editor\'s MCP output logs with the upgrade command to run. This check is cached for 24 hours and never blocks server startup.\n\n## Usage Documentation\n\nThis is the Bridge API tooling worth knowing about as a software engineer \u2014 the things you\'d ask an agent to do \u2014 grouped by how often you would use them. Each entry covers **what it does**, **when it\'s useful** and **how to use it**. The behind-the-scenes plumbing is summarized at the end under [Extra Capabilities](#extra-capabilities), and a full enumeration lives in [Reference](#reference).\n\nFor invocation, prefer the slash command \u2014 it\'s deterministic. A free-text example is shown only where natural-language phrasing reliably maps to the right automation; high-consequence or easily-misread automations show only the slash command on purpose.\n\n### Tier 1 \u2014 Regularly useful\n\nThese features are useful for most tickets.\n\n**1. Review Ticket**\n- **What it does:** Runs a full quality review of a ticket: clarifying questions + critique (initial round), an automatic alternate-model second opinion, then evaluates findings and produces a decision page to accept/reject them. The second-opinion pass is included by default and can be skipped with `--rounds=1`.\n- **When it\'s useful:** (Refinement) Right after a ticket is drafted, before anyone starts building \u2014 to surface gaps and tighten it.\n- **How to use it:** `/review-ticket BAPI-123` (command only \u2014 "review" as free text is easily mistaken for a freehand agent review).\n- **Flags:** `--auto` auto-accept findings / skip the approval gates \xB7 `--rounds=1` skip the automatic second-opinion review step while preserving downstream evaluation and decision-capture work (cheaper single-pass review). `--rounds=2` (default) runs the full two-round review.\n- **Multi-ticket fan-out:** `/review-tickets BAPI-123 BAPI-456` opens one terminal tab per ticket for parallel review with no worktrees (terminal launcher only \u2014 no `wt`/`git`). All `/review-ticket` flags apply; `--review KEY=auto,rounds=N` sets per-ticket overrides. Packaged CLI: `npx -y @bridge_gpt/mcp-server review-tickets KEY [KEY ...]`.\n\n**2. Start Tickets**\n- **What it does:** Creates one git worktree per ticket and spawns an agent session in each to implement them in parallel.\n- **When it\'s useful:** (Implementation | Automation) When you\'re ready to start building one or more refined tickets concurrently.\n- **How to use it:** `/start-tickets BAPI-248 BAPI-250` (see [CLI Subcommands](#cli-subcommands) for the full flag table and cross-platform behavior).\n- **Flags:** `--auto` skip the approval gates \xB7 `--base-branch <branch>` branch off something other than the default.\n\n**3. Brainstorm**\n- **What it does:** Fans your problem out to two different LLMs and returns their approaches directly. Runs in one of three modes, selected via `mode`: **`technical`** (default \u2014 implementation/architecture approaches), **`design`** (UI/UX and visual direction), or **`discovery`** (stakeholder discovery questions for early/vague tasks, grouped into `Technical Discovery Questions` and `Business / Stakeholder Discovery Questions` and tagged `[HUMAN]`/`[CODE]`/`[TICKET]`). Discovery needs no extra configuration. The legacy boolean `design=true` still works and maps to `mode: "design"`.\n- **When it\'s useful:** (Architecture | Refinement) Early, when you want a spread of approaches \u2014 `technical` for *how to build it*, `design` for *how it should look*, `discovery` for *what we still need to figure out* before a real ticket exists.\n- **How to use it:** ask your agent to brainstorm \u2014 *"Brainstorm approaches for adding rate limiting to the LLM client; fan it out to multiple models."* For a design pass: *"Run a design brainstorm for the evidence-freshness dashboard UI."* For early discovery: *"Run a discovery brainstorm \u2014 `request_brainstorm` with `mode: "discovery"` \u2014 for this vague request so we can collect the questions stakeholders need to answer first."*\n\n**4. Deep Research**\n- **What it does:** Runs multi-source, fact-checked web research on a technical topic and returns a cited report.\n- **When it\'s useful:** (Architecture | Refinement) When a decision hinges on outside knowledge (libraries, best practices, standards) you don\'t already have.\n- **How to use it:** `/bridge-research <question>`\n\n**5. Jira Ticket Writer**\n- **What it does:** An agent that drafts a well-structured Jira ticket from a plain description, applying your project\'s standards.\n- **When it\'s useful:** (Refinement) When you have an idea in your head and want a properly-formatted ticket draft without writing it by hand.\n- **How to use it:** `/write-ticket <description>` (or ask your agent) \u2014 *"Use the jira ticket writer to turn our conversation into a ticket."*\n- **Flags:** `--standards <path>` apply a specific standards file when drafting.\n\n**6. Upload Ticket**\n- **What it does:** Pushes a drafted ticket up to Jira as a real issue (the `create_ticket` capability); handles markdown and child tickets under an Epic.\n- **When it\'s useful:** (Refinement) The final step after drafting \u2014 to get the ticket into Jira so it can be tracked and worked.\n- **How to use it:** Ask your agent to create the ticket, and it should confirm before creating the live Jira issue.\n- **Options:** Describe the issue type (Bug / Story / Task / Epic) and, for a child under an Epic, the parent key.\n\n### Tier 2 \u2014 Occasionally useful\n\nThese features are good to know, but you probably won\'t use them every day.\n\n**1. Plan Ticket**\n- **What it does:** Generates a step-by-step implementation plan for a ticket, with references to real code files, and saves it locally.\n- **When it\'s useful:** (Refinement | Implementation) Once a ticket is solid and you want a concrete build plan before (or instead of) auto-implementing.\n- **How to use it:** `/plan-ticket BAPI-123`\n- **Flags:** `--provider <name>` choose the model provider \xB7 `--second-opinion <provider>` cross-check the plan with a second provider.\n\n**2. Clarify Ticket**\n- **What it does:** Generates clarifying questions for a ticket (or debugging guidance for bugs) and saves them locally.\n- **When it\'s useful:** (Refinement) When a ticket feels under-specified and you want the open questions made explicit.\n- **How to use it:** `/clarify-ticket BAPI-123` \u2014 *"Generate clarifying questions for BAPI-123"*\n- **Flags:** `--provider <name>` choose the model provider \xB7 `--second-opinion <provider>` cross-check with a second provider.\n\n**3. Critique Ticket**\n- **What it does:** Critiques a ticket\'s quality against your project standards and lists deviations + improvements.\n- **When it\'s useful:** (Refinement) When you want a quality gate on a ticket before it\'s worked.\n- **How to use it:** `/critique-ticket BAPI-123` \u2014 *"Critique BAPI-123 against our project standards and list what\'s missing or deviating."*\n- **Flags:** `--provider <name>` choose the model provider \xB7 `--second-opinion <provider>` cross-check with a second provider.\n\n**4. Create Doc**\n- **What it does:** Generates a design document for a ticket \u2014 a TDD (technical design, engineer audience), an FSD (functional spec, for product/design/QA), or a PRD (product requirements: problem, goals, success metrics) \u2014 and saves it locally.\n- **When it\'s useful:** (Architecture | Refinement) When a ticket needs a fuller design write-up before planning or implementation, in the shape that fits your audience.\n- **How to use it:** `/create-doc BAPI-123 --doc-type tdd` (or `fsd` / `prd`)\n- **Flags:** `--doc-type tdd|fsd|prd` which document to generate (required) \xB7 `--provider <name>` choose the model provider \xB7 `--second-opinion <provider>` cross-check with a second provider.\n\n**5. Explore Ticket**\n- **What it does:** Explores the codebase for a task and recommends implementation options or surfaces clarifying questions, with optional research.\n- **When it\'s useful:** (Architecture | Refinement) Before writing a ticket or plan, when you\'re unsure how a change would fit the existing code.\n- **How to use it:** `/explore-ticket <task>` \u2014 *"Explore the codebase for how we\'d add a Mistral LLM provider and recommend 2\u20133 implementation options."*\n\n**6. Second Opinion**\n- **What it does:** Gets an immediate critique of any text from a different model family \u2014 no artifact saved, just the reply.\n- **When it\'s useful:** (Architecture | Refinement | Implementation) Any time you want a quick sanity check on a plan, draft, or decision from a fresh perspective.\n- **How to use it:** ask your agent \u2014 *"Get a second opinion from Gemini on whether the BAPI-123 plan\'s migration step is safe to run against prod."*\n- **Options:** pick the provider (anthropic / openai / gemini) and the tier (cheap / basic / premium).\n\n**7. Generate Image**\n- **What it does:** Generates an image from a text prompt using a provider image model (OpenAI `gpt-image-2` by default, or Google Imagen) and returns the image directly. Spends provider credits on every call.\n- **When it\'s useful:** (Architecture | Refinement) When you want a quick visual \u2014 a UI mockup, diagram, or illustration \u2014 to anchor a design discussion or attach to a ticket.\n- **How to use it:** ask your agent \u2014 *"Generate an image of a dashboard showing SOC2 evidence freshness as a traffic-light grid."*\n- **Options:** `provider` openai (`gpt-image-2`) / gemini (Imagen \u2014 adds an invisible SynthID watermark) \xB7 `quality` low (default, cheapest) / medium / high \xB7 `size` 1024x1024 / 1024x1536 / 1536x1024. The image is always saved to `BAPI_DOCS_DIR/images/` and also returned inline.\n\n**8. Implement Ticket**\n- **What it does:** Full build for one ticket: generate a plan, write the code, commit, open a PR, and monitor CI.\n- **When it\'s useful:** (Implementation) When a ticket is ready and you want it taken from plan to open PR in one go.\n- **How to use it:** `/implement-ticket BAPI-123` (command only \u2014 "implement X" as free text almost always triggers a freehand build instead of the Bridge plan\u2192code\u2192PR\u2192CI pipeline).\n- **Flags:** `--auto` skip the approval gates (e.g. auto-commit/push).\n\n**9. Full Automation**\n- **What it does:** Drives the whole chain end-to-end: idea \u2192 ticket(s) \u2192 review each \u2192 spawn worktrees to implement.\n- **When it\'s useful:** (Automation) When you want to go from a raw idea to in-progress implementation with minimal hands-on steps.\n- **How to use it:** `/full-automation <idea>` (command only \u2014 creates tickets, spawns worktrees, and carries scheduling/`--max-children` flags that free text can\'t).\n- **Flags:** `--require-approval` toggle the approval gates, full automation runs end to end by default.\n\n**10. Idea to Ticket**\n- **What it does:** Turns a one-line idea into a Jira Task/Spike (or an Epic plus child tickets), with research, duplicate detection, and a critique pass built in.\n- **When it\'s useful:** (Refinement | Automation) When you have a rough idea and want a fully-formed, uploaded ticket without the manual draft-and-refine loop.\n- **How to use it:** `/idea-to-ticket <idea>`\n\n### Tier 3 \u2014 Now and then\n\nThese features are useful once in a while, but you probably won\'t need them everyday.\n\n**1. Reimplement Ticket**\n- **What it does:** Pulls in new context/attachments since the last pass and implements small follow-up changes on an already-built ticket.\n- **When it\'s useful:** (Implementation) After review feedback or new screenshots, when you need a targeted second pass rather than a fresh build.\n- **How to use it:** `/reimplement-ticket BAPI-123`\n\n**2. Run Tests**\n- **What it does:** Runs the unit and E2E suites and autonomously triages/fixes failures (via the test-correction agent).\n- **When it\'s useful:** (Implementation) After making changes, to confirm everything passes and auto-fix straightforward breakages.\n- **How to use it:** `/run-tests` (`--unit-only`, `--skip-e2e`)\n\n**3. Plan Epic**\n- **What it does:** Decomposes a large epic into sub-tasks with a structured exploration doc for each.\n- **When it\'s useful:** (Architecture | Refinement) When a feature is too big for one ticket and you need it broken down and scoped.\n- **How to use it:** `/plan-epic <epic>` \u2014 *"Decompose the epic \'migrate PayPal token storage off Custom Objects\' into sub-tasks with an exploration doc for each."*\n\n**4. Update Ticket**\n- **What it does:** Synthesizes a ticket\'s clarifying answers and critique into a rewritten description and pushes it to Jira.\n- **When it\'s useful:** (Refinement) After review, to fold the resolved questions and fixes back into the ticket itself.\n- **How to use it:** `/update-ticket BAPI-123` (command only \u2014 does a full overwrite of the live Jira description; "update" as free text is both vague and hard to reverse).\n\n**5. Get Ticket**\n- **What it does:** Retrieves the full details of a Jira ticket (summary, status, description, etc.).\n- **When it\'s useful:** (Refinement | Implementation) Any time you want the agent to read a ticket before acting on it.\n- **How to use it:** ask your agent \u2014 *"Pull up BAPI-123 and show me its description, status, and acceptance criteria."*\n\n**6. Write Comment**\n- **What it does:** Posts a comment on a Jira ticket (markdown; long ones can attach as a file).\n- **When it\'s useful:** (Refinement | Implementation) To leave context, status, or a decision trail on the ticket.\n- **How to use it:** ask your agent \u2014 *"Post a comment on BAPI-123: blocked on the expired Atlassian token \u2014 will retry after it\'s rotated."*\n\n**7. Download / Upload Attachment**\n- **What it does:** Pulls files off a Jira ticket to disk, or attaches a local file to a ticket.\n- **When it\'s useful:** (Refinement | Implementation) When a ticket has design files/logs you need locally, or you want to attach output back to it.\n- **How to use it:** ask your agent \u2014 *"Download the design mockups attached to BAPI-123 into my docs folder."* / *"Attach build-log.txt to BAPI-123."*\n\n**8. Learn Repository**\n- **What it does:** Researches and documents the repo\'s architecture, testing, review, and correctness standards, then saves them to Bridge for future agents.\n- **When it\'s useful:** (Setup/Learning) When onboarding a new repo, or after big changes, so Bridge\'s agents follow your conventions.\n- **How to use it:** `/learn-repository`\n\n**9. Teach Bridge**\n- **What it does:** Takes a plain-English instruction, figures out which standards field it belongs to, and merges it in (admin only).\n- **When it\'s useful:** (Setup/Learning) When you notice the agents missing a convention and want to correct it in one sentence.\n- **How to use it:** `/teach-bridge <teaching>` \u2014 *"Teach Bridge: always use data-testid selectors in E2E tests."*\n\n### Operational commands\n\nWorkflow commands you\'ll reach for during implementation and CI, beyond the tiers above:\n\n| Command | What it does |\n|---|---|\n| `/code-ticket PROJ-123` | Download the implementation plan and questions, then execute the plan inline |\n| `/commit-ticket PROJ-123` | Stage, commit, and push changes; transition Jira status; post a smoke-test comment |\n| `/create-pr PROJ-123` | Commit staged changes and open a pull request |\n| `/check-ci [PROJ-123]` | Monitor CI checks for the current branch, triage failures, apply fixes, and report results |\n| `/parse-repository` | Queue a background job to index the repository for Bridge AI agents |\n| `/check-parse-status` | Check whether a background repository parse job is still running |\n| `/scan-tickets` | Sync recently-updated Jira tickets and backfill workflow timestamps |\n\n> Commands are designed for Claude Code. Other editors may support slash commands differently \u2014 check your editor\'s documentation for how to invoke prompt files.\n\n### Extra Capabilities\n\nBehind-the-scenes capabilities an agent gains from the MCP tools \u2014 mostly invoked automatically by the commands above, rarely requested by name:\n\n- **Ship a PR end-to-end:** commit & push, open a pull request, transition the Jira status, and discover/poll CI checks (powers `commit-ticket`, `create-pr`, `check-ci`, `implement-ticket`).\n- **Architecture plan** for a ticket (design-level guidance, separate from the implementation plan).\n- **Index the codebase** so Bridge\'s agents can reason about it: queue/parse the repo, check parse status, regenerate the directory map.\n- **Read & tune project config/standards:** list/read/update config fields, fetch project standards, and the per-topic `learn-*` commands that populate them.\n- **Ticket lifecycle bookkeeping:** track tickets and backfill workflow-state timestamps (`scan-tickets`), search across tickets, read comments, list attachments.\n- **Pipeline machinery:** list/inspect pipeline recipes and run/resume/list/delete pipeline runs (the engine under the orchestration commands).\n- **Decision page** generation for capturing human review decisions as structured data.\n- **Connectivity & identity checks:** ping Bridge, check your role, resolve the local docs directory.\n- **Retrieve any generated artifact** (`get_*` for plans, critiques, questions, brainstorms, research, architecture) without regenerating it.\n- **Tiered-section execution telemetry** recording (internal measurement).\n\n## CLI Subcommands\n\nBeyond `--init` / `--upgrade`, the package ships operational subcommands of the **single `bridge-api-mcp-server` bin** (not separate binaries) \u2014 so they travel with the package to every consumer. See [Usage Documentation \u2192 Start Tickets](#tier-1--regularly-useful) for *when* to use `start-tickets`; this section is the full CLI reference.\n\n### `start-tickets`\n\nSpawns one Worktrunk worktree + selected-agent session per Jira ticket and backs the `/start-tickets` slash command. The agent defaults to **Claude Code** (`claude`) and is configurable via `--agent`.\n\n```\nnpx -y @bridge_gpt/mcp-server start-tickets [flags] KEY [KEY ...]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--agent claude\\|cursor-agent` | `claude` | Agent command to launch in each worktree |\n| `--terminal terminal\\|iterm` | auto-detect via `$TERM_PROGRAM` | Override the macOS terminal app (honored on macOS only) |\n| `--dry-run` | off | Print intended actions; create no worktrees, open no tabs (any OS) |\n| `--branch KEY=BRANCH` | `feature/<KEY>` | Use a custom branch for that ticket (repeatable) |\n| `--base-branch <BRANCH>` | `main` | Cut new worktrees from `<BRANCH>` and refresh `origin/<BRANCH>` instead of `main` |\n| `--no-refresh-main` | off (the configured base branch is refreshed) | Skip refresh of the configured base branch (default `main`). Historical flag name preserved for backward compatibility \u2014 despite the name, it now skips refresh of whatever `--base-branch` resolves to. |\n| `--max-parallel N` | `3` | Max worktrees created concurrently |\n| `--conductor` | off | Opt into the Conductor system (per-worker `BAPI_CONDUCTOR_*` env + Claude hook injection, a supervisor peer tab, and the `check_messages` message-relay prompt). **Default off** \u2014 a plain run spawns `cd <worktree> && <agent> \'/implement-ticket <KEY>\'`. |\n| `-h`, `--help` | \u2014 | Show usage |\n\nEach `KEY` must match `[A-Z]+-[0-9]+` (e.g., `BAPI-248`). The CLI creates/switches each worktree up front (throttled by `--max-parallel`), then opens one tab/session per successful worktree running the selected agent\'s `\'/implement-ticket <KEY>\'` \u2014 `claude \'/implement-ticket <KEY>\'` by default, or `cursor-agent \'/implement-ticket <KEY>\'` with `--agent cursor-agent`. The `/implement-ticket <KEY>` prompt is unchanged for both agents. To launch Cursor Agent instead of Claude Code:\n\n```\nnpx -y @bridge_gpt/mcp-server start-tickets --agent cursor-agent BAPI-248\n```\n\n**Difficulty-based model routing.** Before launching each agent, the CLI selects an implementation **model tier** from the ticket\'s `difficulty` (1-2 \u2192 cheap, 3-5 \u2192 basic, 6+ \u2192 premium) and injects it as a `--model` flag at the spawn boundary. The Python backend returns only the coarse tier (`GET /jira/tickets/{KEY}/model-tier`, computing + caching difficulty on demand); this CLI alone maps a tier to the agent-specific alias (`claude`: `haiku`/`sonnet`/`opus`; `cursor-agent`: version-suffixed strings validated against `cursor-agent --list-models`). It is gated per repo by `difficulty_model_routing_enabled` (default **ON**) with an optional `difficulty_model_tier_overrides` JSON map (tier \u2192 alias). Routing is **fail-open**: missing credentials, an evaluation failure/timeout, a backend `fallback`, an invalid/unavailable alias, an unadvertised Cursor model, or an agent without `--model` support all omit `--model` (the agent uses its default) and surface a per-ticket warning rather than failing the spawn. `--dry-run` does **not** fetch tiers or inject `--model`.\n\n**Conductor observability (opt-in via `--conductor`, BAPI-394).** Conductor is **off by default** \u2014 without `--conductor` no `BAPI_CONDUCTOR_*` env, supervisor tab, or message-relay prompt is produced. With `--conductor`, a run mints a single conductor `run_id` and emits one canonical `run.started` event into the local conductor ledger (`~/.config/bridge/events.db`), attributing each worker by `worker_id`, ticket key, and worktree path, and opens a supervisor peer tab. When the selected agent is **Claude Code**, the CLI also injects a conductor lifecycle hook into each created worktree\'s `.claude/settings.local.json` (preserving any existing hooks) so the spawned session streams local `run.started` / `run.stopped` / `agent.notification` (and, with `BAPI_CONDUCTOR_ENABLE_PRE_TOOL_USE=1`, `tool.intent`) events. Per-worker conductor identity is passed only via secret-free environment scoped to that one terminal/tab/session \u2014 no credentials are ever placed in the env, hook command, or run metadata. Override the gate/supervisor labels with `BAPI_CONDUCTOR_GATE_NAME` / `BAPI_CONDUCTOR_SUPERVISOR_MODE`. Inspect the stream with `conductor doctor`. Observability is best-effort: a conductor failure never blocks or aborts a spawn, and `--dry-run` performs no conductor side effects. (Epic-tick dispatch always enables conductor internally, independent of this flag.)\n\n**Cross-platform spawning.** The CLI routes spawning per platform; `--dry-run` previews the platform-correct command form on any OS. An unsupported `process.platform` (not `darwin`/`win32`/`linux`) fails fast with a clear "unsupported platform" message.\n\n- **macOS** \u2014 opens a Terminal.app or iTerm tab via `osascript`.\n- **Windows** \u2014 creates worktrees with **`git-wt`** (Worktrunk\'s winget alias) and opens a tab via **Windows Terminal (`wt.exe new-tab`)**, falling back to **`Start-Process powershell.exe`** when Windows Terminal is absent. Requires **Git for Windows / Git Bash** (Worktrunk runs its `pre-start` / `post-start` hooks via Git Bash). The Worktrunk binary (`git-wt`) and the tab launcher (`wt.exe`) are resolved independently and never conflated.\n- **Linux** \u2014 creates one detached **tmux** session per ticket (pane kept open after the agent exits); attach with `tmux attach -t <session>`. A missing `tmux` produces a clear, actionable error.\n\nPer-OS prerequisites: macOS `wt`, `git`, `osascript`; Windows `git-wt`, Git for Windows / Git Bash, Windows Terminal or PowerShell; Linux `wt`, `git`, `tmux`. Set `BAPI_WORKTRUNK_BIN` to override the Worktrunk executable name/path for nonstandard installs (`doctor` honors it too). The read-only `doctor` subcommand (below) additionally surfaces a missing `uv` \u2014 Worktrunk\'s `pre-start` hook runs `uv`, but live preflight does not check it \u2014 and the selected agent\'s command; run `doctor --agent cursor-agent` to also check `cursor-agent` (it prints `cursor-agent login` as an informational auth reminder).\n\n### `doctor`\n\nThe package also ships a strictly **read-only** `doctor` subcommand that diagnoses the `start-tickets` prerequisites for the current OS without changing anything:\n\n```\nnpx -y @bridge_gpt/mcp-server doctor [--agent <name>]\n```\n\nIt is **read-only**: it never installs anything, modifies your system, adds an npm `postinstall`, spawns a terminal, or starts the MCP server, and there is no `--fix`. For each prerequisite it prints found/missing and, when missing, the exact per-OS install command **as a manual instruction you run yourself**. The checked set is the `start-tickets` preflight prerequisites **plus `uv`** **plus the selected agent\'s command** (`claude` by default, or `cursor-agent` with `--agent cursor-agent`). The Worktrunk binary is probed via the resolved name (honoring `BAPI_WORKTRUNK_BIN`), not a hard-coded one. **Exit code:** `0` when all required prerequisites are present, non-zero when any is missing or the platform is unsupported. A failing `start-tickets` preflight now hints you to run `doctor` for an actionable diagnostics report.\n\n### `conductor install-git-hooks` (BAPI-395)\n\nInstalls local git hooks that opportunistically emit conductor git/PR/CI events into the local ledger:\n\n```\nconductor install-git-hooks [--json]\n```\n\nThe installed hooks are **local, unversioned, opportunistic, and bypassable**: they live in the worktree\'s git hooks directory (resolved via `git rev-parse --git-common-dir`), insert only a clearly-delimited managed block (preserving any existing user hook content), launch the producer **detached in the background** so a commit or ref update is never blocked, and tolerate every failure (`|| true`). A directory that is not a git worktree, or an existing hook that looks binary/unsafe, is left untouched and reported as a **degraded optional capability** \u2014 never a fatal error. The hooks installed are `post-commit` (emits `git.commit_created`) and `reference-transaction` (emits `worktree.changed` for committed ref updates).\n\nMissing hooks do **not** prevent PR/CI gate evaluation \u2014 `conductor doctor` reads hook presence and managed-snippet status **read-only** (a new `git hooks` section / `git_hooks` JSON object alongside the ledger report), and the `wait_for_done_gate` MCP tool drives CI polling and gate evaluation regardless of whether hooks are installed.\n\n#### `conductor_done_gate` config\n\nThe per-repo `conductor_done_gate` config field (read through the existing config-field route) defines the v1 done gate. It supports exactly one condition, `required_ci_checks_green`:\n\n```json\n{\n "enabled": true,\n "conditions": [\n { "type": "required_ci_checks_green", "required_checks": ["build", "test"] }\n ]\n}\n```\n\n`gate.met` is emitted (exactly once per `repo + pr_number + head_sha + effective config`) only when every listed required check is present, complete, and green for the bound PR head SHA. The gate **fails closed**: an unset, disabled (`enabled` not strictly `true`), malformed, empty, or unsupported config emits no `gate.met`.\n\n#### `conductor_auto_merge_enabled` config (C6 conditional auto-merge)\n\nWhen a worker\'s PR meets the done gate (`gate.met`), the supervisor can autonomously merge it \u2014 but **only** when the repo has explicitly opted in. The per-repo `conductor_auto_merge_enabled` config field (read through the same config-field route as `conductor_done_gate`) is the opt-in switch:\n\n```json\n{ "enabled": true }\n```\n\nA bare JSON boolean (`true`) is also accepted. **Auto-merge is disabled by default.** Behavior:\n\n- **Disabled / unset / malformed \u2192 dry-run.** Anything other than `true` or `{"enabled": true}` \u2014 including unset, `false`, `{"enabled": false}`, or any malformed value \u2014 fails **closed**: the supervisor records a `merge.dry_run` event and **no PR is ever merged**.\n- **Enabled \u2192 autonomous merge** when the gate is met and the deterministic guards pass.\n- **Kill-switch.** Set `conductor_auto_merge_enabled` to `false` or remove the field to immediately stop autonomous merges. The protected merge endpoint **independently re-enforces** the flag, so even a conductor that calls it cannot merge while the flag is off.\n\nMerge authority is **deterministic code, never an LLM**. The deterministic guards, all bound to **PR number + expected head SHA (never a branch name)**:\n\n- the per-repo enablement flag (off \u2192 dry-run),\n- the PR is still open,\n- the merge is bound to the PR number plus the expected head SHA \u2014 **head-SHA drift between gate evaluation and merge aborts the merge**,\n- required CI checks are **revalidated green immediately before merge**.\n\nIdempotency is crash-safe and race-safe: a TTL lease keyed by the deterministic action key `merge:{repo}:{pr}:{head_sha}:{gate}` is claimed before acting, and an existing `merge.succeeded` for that key is terminal \u2014 the supervisor never double-merges across a crash/restart or two racing instances. The conductor never holds VCS write credentials: it calls the protected Bridge API endpoint `POST /vcs/pull-requests/{pr_number}/merge`, which owns the privileged merge, and records the returned `merge.dry_run` / `merge.attempted` / `merge.succeeded` / `merge.failed` / `merge.pending_approval` events into the local ledger. `merge.failed` is **retryable** (a drifted head SHA produces a new action key); `merge.pending_approval` is **nonterminal** \u2014 the worker remains active until a human redeems the approval token and the server returns `merge.succeeded`. **`merge.succeeded` is the only terminal merge event.** The local SQLite conductor store uses schema version 5 (BAPI-413) to accommodate the `merge.pending_approval` type in the `events.type` CHECK constraint.\n\n## Custom Pipelines\n\nYou can create your own pipelines by adding JSON files to `.bridge/pipelines/`. Running `--init` scaffolds this directory with a `README.md` and an example pipeline to get you started.\n\nThe easiest way to write a custom pipeline is to describe what you want to automate to your AI coding agent and have it draft the JSON for you. The schema is straightforward, and agents like Claude Code understand it well \u2014 just describe the steps you want, and the agent will produce a working pipeline file.\n\n**What you can build:**\n\n- Any sequence of Bridge MCP tool calls and free-form agent tasks\n- Parameterized workflows using variables (e.g., `{ticket_key}`)\n- Approval gates that pause for user confirmation before sensitive steps\n- Per-step error handling \u2014 halt immediately or warn and continue\n\n**Ideas for custom pipelines:**\n\n- A standup pipeline that fetches your open tickets and summarizes their status\n- A ticket triage pipeline that runs critiques on a batch of new tickets\n- A pre-merge checklist that runs tests, checks linting, and posts a summary comment\n\n**Step types:**\n\n| Type | What it does |\n|---|---|\n| `mcp_call` | Calls an MCP tool with the given params |\n| `agent_task` | Gives the AI a free-form instruction (inline or from a file in `.bridge/instructions/`) |\n\nVariables are declared in the `variables` array and referenced as `{variable_name}` in params and instructions. Each step supports `on_error: "halt"` (default) or `"warn_and_continue"`, and `requires_approval: true` to pause before execution.\n\n**System variables:**\n\nTwo variables are automatically available in every pipeline without declaring them:\n\n| Variable | What it does |\n|---|---|\n| `{provider}` | Routes AI generation to a specific LLM provider (`openai`, `anthropic`, or `gemini`). Pass it through to any `request_*` tool param to control which provider handles that step. Omit it (or leave it empty) to use the project default. |\n| `{second_opinion}` | Runs AI generation through a different provider than the default, acting as a cross-check. Set to `"auto"` to let Bridge pick the second provider automatically. When set, it takes precedence over `{provider}`. Use this when you want two independent AI perspectives on the same task \u2014 for example, running clarifying questions and a critique twice (once with each provider) produces better results than a single pass. |\n\nSee `.bridge/pipelines/README.md` for the full schema reference.\n\nIf a custom pipeline has the same key as a built-in pipeline, the custom version takes precedence (a warning is logged at startup).\n\n## Smoke testing\n\nThe package ships a canonical, **opt-in** in-host smoke-test runbook at\n`smoke-test/SMOKE-TEST.md`. An AI agent running inside your host (Claude Code,\nCursor, Codex, Windsurf, or VS Code/Copilot) executes it to verify that the MCP\nserver actually works end-to-end *inside that host* \u2014 it calls the real tools and\nrecords a PASS/FAIL verdict for each one in a markdown report.\n\n- `smoke-test/SMOKE-TEST.md` **ships with the npm package** and is the\n **canonical** source of truth for the smoke test.\n- The smoke test **adds no MCP tool** and **does not change the registered\n tool surface** (the server still registers its existing 62 tools).\n- It is **opt-in**: default `--init` **does not scaffold `/smoke-test-mcp`**, so\n consumer command palettes are not polluted.\n\n### Running it\n\nYou have two options:\n\n1. **Copy the opt-in command manually.** Copy the packaged command stub into your\n host\'s command directory, then invoke `/smoke-test-mcp`:\n\n ```bash\n # Claude Code\n cp node_modules/@bridge_gpt/mcp-server/smoke-test/smoke-test-mcp.md .claude/commands/smoke-test-mcp.md\n # Cursor\n cp node_modules/@bridge_gpt/mcp-server/smoke-test/smoke-test-mcp.md .cursor/commands/smoke-test-mcp.md\n ```\n\n2. **Open the runbook directly.** Alternatively, open\n `smoke-test/SMOKE-TEST.md` and ask the host agent to execute it.\n\nReports are written to `<BAPI_DOCS_DIR>/smoke-test/REPORT-<host>-<timestamp>.md`.\n\n## Environment Variables\n\n| Variable | Required | Default | Description |\n|---|---|---|---|\n| `BAPI_BASE_URL` | Yes | `https://bridgegpt-api.com` | Bridge API base URL |\n| `BAPI_REPO_NAME` | Yes | _(none)_ | Jira project/repository identifier configured in Bridge API |\n| `BAPI_API_KEY` | Yes | _(none)_ | API key obtained from the Bridge API setup UI |\n| `BAPI_PROJECT_ROOT` | No | _(auto-set by --init)_ | Absolute path to project root. Anchors `BAPI_DOCS_DIR` and `BAPI_PIPELINES_DIR` resolution |\n| `BAPI_DOCS_DIR` | No | `docs/tmp` | Local directory for saving plans, critiques, and research reports |\n| `BAPI_PIPELINES_DIR` | No | `.bridge/pipelines` | Directory for user-defined custom pipeline JSON files |\n| `BAPI_WORKTRUNK_BIN` | No | `wt` (`git-wt` on Windows) | Override the Worktrunk executable name/path used by `start-tickets` for nonstandard installs |\n| `BAPI_TMUX_SESSION` | No | `bridge-start-tickets` | Override the tmux session-name prefix used by `start-tickets` on Linux |\n| `BAPI_MCP_UPGRADE_ADVICE_ENABLED` | No | _(enabled)_ | MCP-local opt-out for proactively surfacing upgrade advice in pipeline recipe preambles. Set to `false`/`0`/`no`/`off`/`disabled` to suppress. Disabling it does **not** change the `/jira/ping` response or server-side upgrade computation \u2014 it only gates the recipe-preamble convention |\n| `BRIDGE_MCP_PROFILE` | No | `core` | Startup-time tool registration profile. Controls which tool groups are registered when the server starts. Valid values: `core` (default \u2014 normal coding tools only), `conductor` (core + 8 conductor/event/supervisor tools), `pipeline-authoring` (core + 5 pipeline run/admin tools \u2014 `get_pipeline_recipe` is NOT gated; it stays in `core` because the recipe-driven slash commands depend on it), `full` (all tools, equivalent to the legacy unconditional registration). Unknown, blank, or malformed values fail safe to `core`. Dynamic mid-session switching via `tools/list_changed` is unsupported \u2014 the profile is resolved once at process startup. **Phase 2b note:** epic/conductor sessions launched via `start-tickets` will automatically inject `BRIDGE_MCP_PROFILE=conductor`; that injection is handled at the spawn boundary and is out of scope for this phase. |\n\n## Worktree credentials and the `mcp-invoke` shim\n\nWhen `start-tickets` creates a git worktree, it provisions a Bridge API MCP\nregistration into that worktree so Claude Code (`.mcp.json`) and Cursor\n(`.cursor/mcp.json`) can reach the server immediately. These registrations are\n**secret-free**: they contain no `env` block and no API key. Instead they point\nat an internal subcommand of the published single CLI bin, `mcp-invoke`:\n\n```bash\nnpx -y @bridge_gpt/mcp-server@<VERSION> mcp-invoke --target bapi --project-root <ABS_WORKTREE_PATH>\n```\n\n`mcp-invoke` is not a separate binary \u2014 it is a positional subcommand of\n`bridge-api-mcp-server`. It resolves the repo identity from the absolute\n`--project-root` (the committed `.bridge/config` manifest, falling back to the\ngit common dir), resolves credentials from the home-directory credential store,\nand then spawns the real MCP server with that environment.\n\n### Credential store\n\nCredentials live outside the repository, keyed by `bapi:<repo_name>`:\n\n```json\n{\n "bapi:<repo_name>": {\n "BAPI_API_KEY": "..."\n }\n}\n```\n\nResolution order:\n\n1. `BAPI_API_KEY` in the parent environment (overrides the file entirely).\n2. `$XDG_CONFIG_HOME/bridge/credentials.json`, else `~/.config/bridge/credentials.json`.\n3. `~/.bridge/credentials.json` (only when the primary path is absent).\n\nOn POSIX systems, lock the file down so only you can read it:\n\n```bash\nchmod 600 ~/.config/bridge/credentials.json\n```\n\n`mcp-invoke` warns (but continues) if the file is group/world-readable, and it\nnever creates or initializes the credential file for you.\n\n### Populating the credential store\n\nThe same store also backs the shell-spawned `start-tickets` CLI (its\ndifficulty\u2192model routing runs in a Bash process that cannot see an `env` block in\n`.mcp.json` / `.cursor/mcp.json`), so the store must hold the key for routing to\nwork. Two supported paths write it for you:\n\n- **Install-time upsert.** `/install-bridge`\'s final stage persists the validated\n routing credential into `~/.config/bridge/credentials.json` (target\n `bapi:<repo>`) via the `persist_routing_credential` MCP tool \u2014 the tool resolves\n the key inside the MCP process and writes the store, so no secret crosses the\n wire.\n- **One-shot migration.** If a key currently lives only in `.mcp.json` /\n `.cursor/mcp.json`, migrate it into the user-scoped store with the consent-gated\n command (a compatibility aid, not a live fallback):\n\n ```bash\n npx -y @bridge_gpt/mcp-server credentials migrate-agent-config [--write-credentials]\n ```\n\n Run it without `--write-credentials` to preview; add the flag to write the store.\n\n## Reference\n\nThe full surface, for when you need the complete enumeration. Day-to-day, use [Usage Documentation](#usage-documentation) instead \u2014 you don\'t call MCP tools directly; you ask your AI assistant to perform a task, or compose tools into a pipeline.\n\n### MCP tools\n\nThe server registers **58 tools**. Async AI tools follow a request/get pattern: call the `request_*` tool to kick off generation, then the matching `get_*` tool to retrieve the result (or pass `wait_for_result: true` to poll automatically).\n\n- **Connectivity & identity** \u2014 `ping`, `get_my_role`, `get_docs_dir`\n- **Jira tickets** \u2014 `get_tickets`, `get_ticket`, `create_ticket`, `update_ticket_description`, `add_comment`, `get_comments`\n- **Attachments** \u2014 `attachment` (operations: `upload`, `download`, `list`)\n- **AI generation (request/get)** \u2014 `request_plan_generation`/`get_plan`, `request_architecture`/`get_architecture`, `create_doc`/`get_doc` (design docs by `doc_type`: tdd/fsd/prd), `request_prd`/`get_prd`, `request_clarifying_questions`/`get_clarifying_questions`, `request_ticket_critique`/`get_ticket_critique`, `request_ticket_review`, `request_reimplement_context`/`get_reimplement_context`, `request_brainstorm`/`get_brainstorm`, `request_deep_research`/`get_deep_research`\n- **Other AI** \u2014 `second_opinion`, `generate_image`, `generate_decision_page`\n- **Ticket lifecycle** \u2014 `track_ticket`, `update_ticket_state`, `get_ticket_state`\n- **Jira status** \u2014 `get_jira_transitions`, `update_jira_status`, `resolve_target_status`\n- **Repository & CI** \u2014 `parse_repository`, `get_parse_status`, `regenerate_directory_map`, `create_pull_request`, `resolve_ci_checks`, `poll_ci_checks`\n- **Pipelines & automation** \u2014 `list_pipelines`, `get_pipeline_recipe`, `run_pipeline`, `resume_pipeline`, `list_pipeline_runs`, `delete_pipeline_run`, `run_full_automation`, `resume_full_automation`\n- **Config** \u2014 `get_project_standards`, `config_field` (operations: `get`, `update`, `list`)\n\n### Bundled pipelines\n\nPipelines are declarative, multi-step workflows your AI agent executes step-by-step \u2014 each a JSON recipe chaining MCP tool calls and free-form agent tasks, with variable substitution, per-step error handling, and optional approval gates. You can also write your own (see [Custom Pipelines](#custom-pipelines)).\n\n| Pipeline | Description | Invoke with |\n|---|---|---|\n| `implement-ticket` | Generate a plan, execute the implementation, commit, open a PR, and monitor CI | `/implement-ticket PROJ-123` |\n| `review-ticket` | Full ticket quality review: initial clarifying questions + critique, automatic second-opinion pass (default), then evaluation and decision capture. Pass `--rounds=1` to skip the second-opinion step (`--rounds=2` is the default). | `/review-ticket PROJ-123` |\n| `idea-to-ticket` | Turn an idea into a Jira Task/Spike (or Epic + children) with research, dedup, and critique | `/idea-to-ticket "<idea>"` |\n| `plan-epic` | Decompose an epic into sub-tasks with a structured exploration doc for each | `/plan-epic "<epic>"` |\n| `full-automation` | Chain: idea \u2192 ticket(s) \u2192 review each \u2192 spawn worktrees to implement | `/full-automation "<idea>"` |\n| `pr-ticket` | Commit changes and open a pull request | `/create-pr PROJ-123` |\n| `check-ci-ticket` | Commit, open a PR, then monitor CI checks until they pass or fail | `/check-ci PROJ-123` |\n| `learn-repository` | Analyze codebase architecture, testing, review, and documentation standards, then upload to Bridge | `/learn-repository` |\n\n### Pipeline response envelope\n\n`run_pipeline`, `resume_pipeline`, and `list_pipeline_runs` share a unified envelope keyed on `status`:\n\n- `completed` \u2014 terminal success; `results` holds per-step output.\n- `needs_agent_task` \u2014 the orchestrator paused. Read `instruction`, perform the task, then call `resume_pipeline` with `pipeline_run_id` and a string `agent_result`.\n- `failed` \u2014 terminal failure. `error_code` is one of `VALIDATION`, `NOT_FOUND`, `EXPIRED`, `REPO_MISMATCH`, `TOOL_ERROR`.\n\nPaused runs auto-expire after an idle TTL (default 24 hours; override with `ttl_seconds`). The TTL is reset on every state transition. List output is metadata-only \u2014 it never includes resolved recipes, params, instructions, results, or agent outputs.\n';
|
|
13070
|
+
var README = '# @bridge_gpt/mcp-server\n\nThe Bridge MCP is an end-to-end accelerator for shipping code within SFCC, powered by [Bridge API](https://bridgegpt-api.com). Works with Claude Code, Github Copilot, Cursor, Windsurf, and OpenAI Codex.\n\n## Contents\n\n- [Getting Started](#getting-started)\n- [Usage Documentation](#usage-documentation)\n - [Tier 1 \u2014 Regularly useful](#tier-1--regularly-useful)\n - [Tier 2 \u2014 Occasionally useful](#tier-2--occasionally-useful)\n - [Tier 3 \u2014 Now and then](#tier-3--now-and-then)\n - [Operational commands](#operational-commands)\n - [Extra Capabilities](#extra-capabilities)\n- [Salesforce B2C Commerce (SFCC) Tools](#salesforce-b2c-commerce-sfcc-tools)\n- [CLI Subcommands](#cli-subcommands)\n- [Custom Pipelines](#custom-pipelines)\n- [Environment Variables](#environment-variables)\n- [Worktree credentials and the `mcp-invoke` shim](#worktree-credentials-and-the-mcp-invoke-shim)\n- [Reference](#reference)\n\nFor advanced epic/multi-agent orchestration, see [CONDUCTOR.md](./CONDUCTOR.md).\n\n## Getting Started\n\n### Quick start\n\nFrom your **project root**, install and connect in one command:\n\n```bash\nnpx -y @bridge_gpt/mcp-server@latest install-bridge\n```\n\n`install-bridge` scaffolds the project, writes your editor\'s MCP config with real\nvalues, verifies connectivity, persists your API key to the user-scoped credential\nstore, and opens a fresh agent session to finish setup (`/install-bridge` then\n`/learn-repository`). The only inputs are an **API key** (generate one on the Bridge\nAPI web UI **Security** page) and a **repo name** \u2014 everything else is derived. Add\n`--dry-run` to preview every step without writing, pinging, or spawning anything.\n\nTo upgrade later, run:\n\n```bash\nnpx -y @bridge_gpt/mcp-server upgrade\n```\n\n`upgrade` re-execs from `@latest`, refreshes all scaffolded artifacts (slash\ncommands, agents, pipelines), rewrites the version pin, and reconnects \u2014 also\navailable as the `/upgrade-bridge` slash command. (The legacy `--upgrade` flag still\nworks as a fallback.)\n\n<details>\n<summary><strong>Installation Instructions</strong></summary>\n\n#### What `install-bridge` does\n\n`install-bridge` collapses the whole setup into a single command. It:\n\n1. **Scaffolds** the project (the same artifacts `--init` writes: slash commands,\n agents, `.bridge/pipelines/`, and secret-free MCP config placeholders).\n2. **Writes the per-host MCP config** (`.mcp.json` / `.cursor/mcp.json` /\n `.vscode/mcp.json`) with your real `BAPI_REPO_NAME` / `BAPI_API_KEY` /\n `BAPI_BASE_URL` / `BAPI_DOCS_DIR`, preserving any unrelated servers. The\n launcher it writes is pinned to the exact installed version so `npx` never\n silently reuses a stale local copy. (Windsurf and Codex are global configs it\n can\'t safely write \u2014 it prints copy-paste instructions for those.)\n3. **Verifies connectivity** against the Bridge API before persisting anything.\n4. **Persists your key** to the user-scoped credential store\n (`~/.config/bridge/credentials.json`, target `bapi:<repo>`) so shell-spawned\n tooling (e.g. `start-tickets`) can resolve it.\n5. **Opens a fresh agent session** that runs `/install-bridge` (to derive the\n remaining config fields from your codebase) and then `/learn-repository`.\n\nThe only inputs are an **API key** and a **repo name** (everything else is\nderived). Resolution order:\n\n- **API key:** `--api-key <key>` \u2192 `BAPI_API_KEY` env \u2192 an interactive (no-echo)\n prompt. Generate one first on the Bridge API web UI **Security** page (see\n [Generate an API Key](#2-generate-an-api-key)); the command consumes a key, it\n never mints one. The key is **never printed or logged**.\n- **Repo name:** `--repo <name>` \u2192 `BAPI_REPO_NAME` env \u2192 an inferred default you\n confirm interactively. It MUST match the server-side repository registration.\n\nUseful flags:\n\n- `--dry-run` \u2014 preview every step (scaffold targets, config files and keys with\n the key value **redacted**, the ping target, the credential store target, and\n the exact agent spawn command) without writing, pinging, or spawning anything.\n- `--force` \u2014 overwrite an existing real `BAPI_API_KEY` in a host config without\n prompting (re-running is otherwise non-destructive).\n- `--agent claude|cursor-agent` \u2014 which agent to launch for the agentic remainder\n (default `claude`).\n\nThat\'s it \u2014 once `install-bridge` finishes you\'re connected. If you prefer to do\nit by hand (or just want to understand each step), the manual flow below does the\nsame thing.\n\n#### Manual Setup (Alternative)\n\n##### 1. Install the Package\n\nFrom your **project root**, install the MCP server and scaffold slash commands:\n\n```bash\nnpm i @bridge_gpt/mcp-server\nnpx -y @bridge_gpt/mcp-server --init\n```\n\n`--init` must be run from the directory containing your `package.json`. It:\n\n- Creates slash commands in `.claude/commands/` and `.cursor/commands/`\n- Detects existing MCP config files and sets `BAPI_PROJECT_ROOT` so local file output resolves correctly\n- Scaffolds `.bridge/pipelines/` for custom pipeline authoring\n\nRe-run `--init` after upgrading the package to get updated commands.\n\n##### 2. Generate an API Key\n\n1. Log in to [Bridge API](https://bridgegpt-api.com) and navigate to your project\'s **Security** page\n2. Click **Create New Key**\n3. Enter your email, an optional label (e.g., "MCP Server"), and select the **Admin** role\n4. Click **Create Key**\n5. **Copy the key immediately** \u2014 it will not be shown again\n\n##### 3. Configure the MCP Server\n\nAdd the following to your editor\'s MCP configuration file, pasting in the API key from step 2:\n\n<details>\n<summary><strong>Claude Code (.mcp.json)</strong></summary>\n\nThe `--init` command (step 1) detects Claude Code and creates a `.mcp.json` at your project root with placeholder values. Open it and replace `your-repo` and `your-api-key` with your actual values from step 2:\n\n```json\n{\n "mcpServers": {\n "bridge-api": {\n "command": "npx",\n "args": ["-y", "@bridge_gpt/mcp-server"],\n "env": {\n "BAPI_BASE_URL": "https://bridgegpt-api.com",\n "BAPI_REPO_NAME": "your-repo",\n "BAPI_API_KEY": "your-api-key",\n "BAPI_DOCS_DIR": "docs/tmp"\n }\n }\n }\n}\n```\n</details>\n\n<details>\n<summary><strong>VS Code / Copilot (.vscode/mcp.json)</strong></summary>\n\n```json\n{\n "servers": {\n "bridge-api": {\n "command": "npx",\n "args": ["-y", "@bridge_gpt/mcp-server"],\n "env": {\n "BAPI_BASE_URL": "https://bridgegpt-api.com",\n "BAPI_REPO_NAME": "your-repo",\n "BAPI_API_KEY": "your-api-key",\n "BAPI_DOCS_DIR": "docs/tmp"\n }\n }\n }\n}\n```\n</details>\n\n<details>\n<summary><strong>Cursor (.cursor/mcp.json)</strong></summary>\n\n```json\n{\n "mcpServers": {\n "bridge-api": {\n "command": "npx",\n "args": ["-y", "@bridge_gpt/mcp-server"],\n "env": {\n "BAPI_BASE_URL": "https://bridgegpt-api.com",\n "BAPI_REPO_NAME": "your-repo",\n "BAPI_API_KEY": "your-api-key",\n "BAPI_DOCS_DIR": "docs/tmp"\n }\n }\n }\n}\n```\n\n> If project-local config is not supported in your Cursor version, use `~/.cursor/config/mcp.json` instead.\n</details>\n\n<details>\n<summary><strong>Windsurf (~/.codeium/windsurf/mcp_config.json)</strong></summary>\n\nWindsurf only supports global MCP configuration.\n\n```json\n{\n "mcpServers": {\n "bridge-api": {\n "command": "npx",\n "args": ["-y", "@bridge_gpt/mcp-server"],\n "env": {\n "BAPI_BASE_URL": "https://bridgegpt-api.com",\n "BAPI_REPO_NAME": "your-repo",\n "BAPI_API_KEY": "your-api-key",\n "BAPI_DOCS_DIR": "docs/tmp"\n }\n }\n }\n}\n```\n</details>\n\n<details>\n<summary><strong>OpenAI Codex (~/.codex/config.toml)</strong></summary>\n\n```toml\n[mcp_servers.bridge-api]\ncommand = "npx"\nargs = ["-y", "@bridge_gpt/mcp-server"]\n\n[mcp_servers.bridge-api.env]\nBAPI_BASE_URL = "https://bridgegpt-api.com"\nBAPI_REPO_NAME = "your-repo"\nBAPI_API_KEY = "your-api-key"\nBAPI_DOCS_DIR = "docs/tmp"\n```\n\n> Codex users: set `BAPI_PROJECT_ROOT` manually in your config (see Environment Variables below).\n</details>\n\nAfter saving the config, restart your editor or reload the MCP server connection. Verify connectivity by asking your AI assistant to call the `ping` tool.\n\n##### 4. First-Time Setup: Teach Bridge Your Codebase\n\nIf you\'re the first person to install Bridge API on your project, run the `/learn-repository` slash command after completing setup. This analyzes your codebase\'s architecture, testing patterns, code review standards, and documentation conventions, then uploads the findings to Bridge API. This gives Bridge the context it needs to generate implementation plans, ticket critiques, and code reviews that are consistent with your project\'s actual architecture and conventions.\n\nYou only need to do this once per project \u2014 the learned standards persist for all team members.\n\n##### Upgrading (details)\n\nThe one-command `npx -y @bridge_gpt/mcp-server upgrade` (shown above) is the\nrecommended path. The legacy flag `npx -y @bridge_gpt/mcp-server --upgrade` still\nworks: it runs `npm i @bridge_gpt/mcp-server@latest`, prints a before/after version\nsummary, then re-runs the full `--init` scaffolding flow to update your slash\ncommands, agents, and pipeline definitions.\n\nThe MCP server also checks for updates automatically on startup. If a newer version is available, you\'ll see a notice in your editor\'s MCP output logs with the upgrade command to run. This check is cached for 24 hours and never blocks server startup.\n\n</details>\n\n## Usage Documentation\n\nThis is the Bridge API tooling worth knowing about as a software engineer \u2014 the things you\'d ask an agent to do \u2014 grouped by how often you would use them. Each entry covers **what it does**, **when it\'s useful** and **how to use it**. The behind-the-scenes plumbing is summarized at the end under [Extra Capabilities](#extra-capabilities), and a full enumeration lives in [Reference](#reference).\n\nWorking in a Salesforce B2C Commerce codebase? Bridge also ships read-only SFCC platform-introspection tools \u2014 see [Salesforce B2C Commerce (SFCC) Tools](#salesforce-b2c-commerce-sfcc-tools).\n\nFor invocation, prefer the slash command \u2014 it\'s deterministic. A free-text example is shown only where natural-language phrasing reliably maps to the right automation; high-consequence or easily-misread automations show only the slash command on purpose.\n\n### Tier 1 \u2014 Regularly useful\n\nThese features are useful for most tickets.\n\n**1. Review Ticket**\n- **What it does:** Runs a full quality review of a ticket: clarifying questions + critique (initial round), an automatic alternate-model second opinion, then evaluates findings and produces a decision page to accept/reject them. The second-opinion pass is included by default and can be skipped with `--rounds=1`.\n- **When it\'s useful:** (Refinement) Right after a ticket is drafted, before anyone starts building \u2014 to surface gaps and tighten it.\n- **How to use it:** `/review-ticket BAPI-123` (command only \u2014 "review" as free text is easily mistaken for a freehand agent review).\n- **Flags:** `--auto` auto-accept findings / skip the approval gates \xB7 `--rounds=1` skip the automatic second-opinion review step while preserving downstream evaluation and decision-capture work (cheaper single-pass review). `--rounds=2` (default) runs the full two-round review.\n- **Multi-ticket fan-out:** `/review-tickets BAPI-123 BAPI-456` opens one terminal tab per ticket for parallel review with no worktrees (terminal launcher only \u2014 no `wt`/`git`). All `/review-ticket` flags apply; `--review KEY=auto,rounds=N` sets per-ticket overrides. Packaged CLI: `npx -y @bridge_gpt/mcp-server review-tickets KEY [KEY ...]`.\n\n**2. Start Tickets**\n- **What it does:** Creates one git worktree per ticket and spawns an agent session in each to implement them in parallel.\n- **When it\'s useful:** (Implementation | Automation) When you\'re ready to start building one or more refined tickets concurrently.\n- **How to use it:** `/start-tickets BAPI-248 BAPI-250` (see [CLI Subcommands](#cli-subcommands) for the full flag table and cross-platform behavior).\n- **Flags:** `--auto` skip the approval gates \xB7 `--base-branch <branch>` branch off something other than the default.\n\n**3. Brainstorm**\n- **What it does:** Fans your problem out to two different LLMs and returns their approaches directly. Runs in one of three modes, selected via `mode`: **`technical`** (default \u2014 implementation/architecture approaches), **`design`** (UI/UX and visual direction), or **`discovery`** (stakeholder discovery questions for early/vague tasks, grouped into `Technical Discovery Questions` and `Business / Stakeholder Discovery Questions` and tagged `[HUMAN]`/`[CODE]`/`[TICKET]`). Discovery needs no extra configuration. The legacy boolean `design=true` still works and maps to `mode: "design"`.\n- **When it\'s useful:** (Architecture | Refinement) Early, when you want a spread of approaches \u2014 `technical` for *how to build it*, `design` for *how it should look*, `discovery` for *what we still need to figure out* before a real ticket exists.\n- **How to use it:** ask your agent to brainstorm \u2014 *"Brainstorm approaches for adding rate limiting to the LLM client; fan it out to multiple models."* For a design pass: *"Run a design brainstorm for the evidence-freshness dashboard UI."* For early discovery: *"Run a discovery brainstorm \u2014 `request_brainstorm` with `mode: "discovery"` \u2014 for this vague request so we can collect the questions stakeholders need to answer first."*\n\n**4. Deep Research**\n- **What it does:** Runs multi-source, fact-checked web research on a technical topic and returns a cited report.\n- **When it\'s useful:** (Architecture | Refinement) When a decision hinges on outside knowledge (libraries, best practices, standards) you don\'t already have.\n- **How to use it:** `/bridge-research <question>`\n\n**5. Jira Ticket Writer**\n- **What it does:** An agent that drafts a well-structured Jira ticket from a plain description, applying your project\'s standards.\n- **When it\'s useful:** (Refinement) When you have an idea in your head and want a properly-formatted ticket draft without writing it by hand.\n- **How to use it:** `/write-ticket <description>` (or ask your agent) \u2014 *"Use the jira ticket writer to turn our conversation into a ticket."*\n- **Flags:** `--standards <path>` apply a specific standards file when drafting.\n\n**6. Upload Ticket**\n- **What it does:** Pushes a drafted ticket up to Jira as a real issue (the `create_ticket` capability); handles markdown and child tickets under an Epic.\n- **When it\'s useful:** (Refinement) The final step after drafting \u2014 to get the ticket into Jira so it can be tracked and worked.\n- **How to use it:** Ask your agent to create the ticket, and it should confirm before creating the live Jira issue.\n- **Options:** Describe the issue type (Bug / Story / Task / Epic) and, for a child under an Epic, the parent key.\n\n### Tier 2 \u2014 Occasionally useful\n\nThese features are good to know, but you probably won\'t use them every day.\n\n**1. Plan Ticket**\n- **What it does:** Generates a step-by-step implementation plan for a ticket, with references to real code files, and saves it locally.\n- **When it\'s useful:** (Refinement | Implementation) Once a ticket is solid and you want a concrete build plan before (or instead of) auto-implementing.\n- **How to use it:** `/plan-ticket BAPI-123`\n- **Flags:** `--provider <name>` choose the model provider \xB7 `--second-opinion <provider>` cross-check the plan with a second provider.\n\n**2. Clarify Ticket**\n- **What it does:** Generates clarifying questions for a ticket (or debugging guidance for bugs) and saves them locally.\n- **When it\'s useful:** (Refinement) When a ticket feels under-specified and you want the open questions made explicit.\n- **How to use it:** `/clarify-ticket BAPI-123` \u2014 *"Generate clarifying questions for BAPI-123"*\n- **Flags:** `--provider <name>` choose the model provider \xB7 `--second-opinion <provider>` cross-check with a second provider.\n\n**3. Critique Ticket**\n- **What it does:** Critiques a ticket\'s quality against your project standards and lists deviations + improvements.\n- **When it\'s useful:** (Refinement) When you want a quality gate on a ticket before it\'s worked.\n- **How to use it:** `/critique-ticket BAPI-123` \u2014 *"Critique BAPI-123 against our project standards and list what\'s missing or deviating."*\n- **Flags:** `--provider <name>` choose the model provider \xB7 `--second-opinion <provider>` cross-check with a second provider.\n\n**4. Create Doc**\n- **What it does:** Generates a design document for a ticket \u2014 a TDD (technical design, engineer audience), an FSD (functional spec, for product/design/QA), or a PRD (product requirements: problem, goals, success metrics) \u2014 and saves it locally.\n- **When it\'s useful:** (Architecture | Refinement) When a ticket needs a fuller design write-up before planning or implementation, in the shape that fits your audience.\n- **How to use it:** `/create-doc BAPI-123 --doc-type tdd` (or `fsd` / `prd`)\n- **Flags:** `--doc-type tdd|fsd|prd` which document to generate (required) \xB7 `--provider <name>` choose the model provider \xB7 `--second-opinion <provider>` cross-check with a second provider.\n\n**5. Explore Ticket**\n- **What it does:** Explores the codebase for a task and recommends implementation options or surfaces clarifying questions, with optional research.\n- **When it\'s useful:** (Architecture | Refinement) Before writing a ticket or plan, when you\'re unsure how a change would fit the existing code.\n- **How to use it:** `/explore-ticket <task>` \u2014 *"Explore the codebase for how we\'d add a Mistral LLM provider and recommend 2\u20133 implementation options."*\n\n**6. Second Opinion**\n- **What it does:** Gets an immediate critique of any text from a different model family \u2014 no artifact saved, just the reply.\n- **When it\'s useful:** (Architecture | Refinement | Implementation) Any time you want a quick sanity check on a plan, draft, or decision from a fresh perspective.\n- **How to use it:** ask your agent \u2014 *"Get a second opinion from Gemini on whether the BAPI-123 plan\'s migration step is safe to run against prod."*\n- **Options:** pick the provider (anthropic / openai / gemini) and the tier (cheap / basic / premium).\n\n**7. Generate Image**\n- **What it does:** Generates an image from a text prompt using a provider image model (OpenAI `gpt-image-2` by default, or Google Imagen) and returns the image directly. Spends provider credits on every call.\n- **When it\'s useful:** (Architecture | Refinement) When you want a quick visual \u2014 a UI mockup, diagram, or illustration \u2014 to anchor a design discussion or attach to a ticket.\n- **How to use it:** ask your agent \u2014 *"Generate an image of a dashboard showing SOC2 evidence freshness as a traffic-light grid."*\n- **Options:** `provider` openai (`gpt-image-2`) / gemini (Imagen \u2014 adds an invisible SynthID watermark) \xB7 `quality` low (default, cheapest) / medium / high \xB7 `size` 1024x1024 / 1024x1536 / 1536x1024. The image is always saved to `BAPI_DOCS_DIR/images/` and also returned inline.\n\n**8. Implement Ticket**\n- **What it does:** Full build for one ticket: generate a plan, write the code, commit, open a PR, and monitor CI.\n- **When it\'s useful:** (Implementation) When a ticket is ready and you want it taken from plan to open PR in one go.\n- **How to use it:** `/implement-ticket BAPI-123` (command only \u2014 "implement X" as free text almost always triggers a freehand build instead of the Bridge plan\u2192code\u2192PR\u2192CI pipeline).\n- **Flags:** `--auto` skip the approval gates (e.g. auto-commit/push).\n\n**9. Full Automation**\n- **What it does:** Drives the whole chain end-to-end: idea \u2192 ticket(s) \u2192 review each \u2192 spawn worktrees to implement.\n- **When it\'s useful:** (Automation) When you want to go from a raw idea to in-progress implementation with minimal hands-on steps.\n- **How to use it:** `/full-automation <idea>` (command only \u2014 creates tickets, spawns worktrees, and carries scheduling/`--max-children` flags that free text can\'t).\n- **Flags:** `--require-approval` toggle the approval gates, full automation runs end to end by default.\n\n**10. Idea to Ticket**\n- **What it does:** Turns a one-line idea into a Jira Task/Spike (or an Epic plus child tickets), with research, duplicate detection, and a critique pass built in.\n- **When it\'s useful:** (Refinement | Automation) When you have a rough idea and want a fully-formed, uploaded ticket without the manual draft-and-refine loop.\n- **How to use it:** `/idea-to-ticket <idea>`\n\n### Tier 3 \u2014 Now and then\n\nThese features are useful once in a while, but you probably won\'t need them everyday.\n\n**1. Reimplement Ticket**\n- **What it does:** Pulls in new context/attachments since the last pass and implements small follow-up changes on an already-built ticket.\n- **When it\'s useful:** (Implementation) After review feedback or new screenshots, when you need a targeted second pass rather than a fresh build.\n- **How to use it:** `/reimplement-ticket BAPI-123`\n\n**2. Run Tests**\n- **What it does:** Runs the unit and E2E suites and autonomously triages/fixes failures (via the test-correction agent).\n- **When it\'s useful:** (Implementation) After making changes, to confirm everything passes and auto-fix straightforward breakages.\n- **How to use it:** `/run-tests` (`--unit-only`, `--skip-e2e`)\n\n**3. Plan Epic**\n- **What it does:** Decomposes a large epic into sub-tasks with a structured exploration doc for each.\n- **When it\'s useful:** (Architecture | Refinement) When a feature is too big for one ticket and you need it broken down and scoped.\n- **How to use it:** `/plan-epic <epic>` \u2014 *"Decompose the epic \'migrate PayPal token storage off Custom Objects\' into sub-tasks with an exploration doc for each."*\n\n**4. Update Ticket**\n- **What it does:** Synthesizes a ticket\'s clarifying answers and critique into a rewritten description and pushes it to Jira.\n- **When it\'s useful:** (Refinement) After review, to fold the resolved questions and fixes back into the ticket itself.\n- **How to use it:** `/update-ticket BAPI-123` (command only \u2014 does a full overwrite of the live Jira description; "update" as free text is both vague and hard to reverse).\n\n**5. Get Ticket**\n- **What it does:** Retrieves the full details of a Jira ticket (summary, status, description, etc.).\n- **When it\'s useful:** (Refinement | Implementation) Any time you want the agent to read a ticket before acting on it.\n- **How to use it:** ask your agent \u2014 *"Pull up BAPI-123 and show me its description, status, and acceptance criteria."*\n\n**6. Write Comment**\n- **What it does:** Posts a comment on a Jira ticket (markdown; long ones can attach as a file).\n- **When it\'s useful:** (Refinement | Implementation) To leave context, status, or a decision trail on the ticket.\n- **How to use it:** ask your agent \u2014 *"Post a comment on BAPI-123: blocked on the expired Atlassian token \u2014 will retry after it\'s rotated."*\n\n**7. Download / Upload Attachment**\n- **What it does:** Pulls files off a Jira ticket to disk, or attaches a local file to a ticket.\n- **When it\'s useful:** (Refinement | Implementation) When a ticket has design files/logs you need locally, or you want to attach output back to it.\n- **How to use it:** ask your agent \u2014 *"Download the design mockups attached to BAPI-123 into my docs folder."* / *"Attach build-log.txt to BAPI-123."*\n\n**8. Learn Repository**\n- **What it does:** Researches and documents the repo\'s architecture, testing, review, and correctness standards, then saves them to Bridge for future agents.\n- **When it\'s useful:** (Setup/Learning) When onboarding a new repo, or after big changes, so Bridge\'s agents follow your conventions.\n- **How to use it:** `/learn-repository`\n\n**9. Teach Bridge**\n- **What it does:** Takes a plain-English instruction, figures out which standards field it belongs to, and merges it in (admin only).\n- **When it\'s useful:** (Setup/Learning) When you notice the agents missing a convention and want to correct it in one sentence.\n- **How to use it:** `/teach-bridge <teaching>` \u2014 *"Teach Bridge: always use data-testid selectors in E2E tests."*\n\n### Operational commands\n\nWorkflow commands you\'ll reach for during implementation and CI, beyond the tiers above:\n\n| Command | What it does |\n|---|---|\n| `/code-ticket PROJ-123` | Download the implementation plan and questions, then execute the plan inline |\n| `/commit-ticket PROJ-123` | Stage, commit, and push changes; transition Jira status; post a smoke-test comment |\n| `/create-pr PROJ-123` | Commit staged changes and open a pull request |\n| `/check-ci [PROJ-123]` | Monitor CI checks for the current branch, triage failures, apply fixes, and report results |\n| `/parse-repository` | Queue a background job to index the repository for Bridge AI agents |\n| `/check-parse-status` | Check whether a background repository parse job is still running |\n| `/scan-tickets` | Sync recently-updated Jira tickets and backfill workflow timestamps |\n\n> Commands are designed for Claude Code. Other editors may support slash commands differently \u2014 check your editor\'s documentation for how to invoke prompt files.\n\n### Extra Capabilities\n\nBehind-the-scenes capabilities an agent gains from the MCP tools \u2014 mostly invoked automatically by the commands above, rarely requested by name:\n\n- **Ship a PR end-to-end:** commit & push, open a pull request, transition the Jira status, and discover/poll CI checks (powers `commit-ticket`, `create-pr`, `check-ci`, `implement-ticket`).\n- **Architecture plan** for a ticket (design-level guidance, separate from the implementation plan).\n- **Index the codebase** so Bridge\'s agents can reason about it: queue/parse the repo, check parse status, regenerate the directory map.\n- **Read & tune project config/standards:** list/read/update config fields, fetch project standards, and the per-topic `learn-*` commands that populate them.\n- **Ticket lifecycle bookkeeping:** track tickets and backfill workflow-state timestamps (`scan-tickets`), search across tickets, read comments, list attachments.\n- **Pipeline machinery:** list/inspect pipeline recipes and run/resume/list/delete pipeline runs (the engine under the orchestration commands).\n- **Decision page** generation for capturing human review decisions as structured data.\n- **Connectivity & identity checks:** ping Bridge, check your role, resolve the local docs directory.\n- **Retrieve any generated artifact** (`get_*` for plans, critiques, questions, brainstorms, research, architecture) without regenerating it.\n- **Tiered-section execution telemetry** recording (internal measurement).\n\n## Salesforce B2C Commerce (SFCC) Tools\n\nSalesforce\'s official MCP server, `@salesforce/b2c-dx-mcp`, covers developer-experience tasks \u2014 cartridge deploy, step debugging, docs search, logs/MRT, and SCAPI Custom API scaffolding. As of its current toolset it has no way to *read* the platform\'s object model, custom objects, or site configuration \u2014 exactly the context an AI coding agent needs to write correct Commerce code and to inspect a sandbox before touching it. Bridge\'s SFCC tools install side-by-side with `b2c-dx-mcp` (they don\'t duplicate its surface) and close that gap with **read-only OCAPI Data API introspection** of system objects, custom object definitions, and site preferences.\n\n**v1 is read-only and developer-sandbox-only** \u2014 no writes, and non-sandbox instances are rejected. Credentials stay local (in `dw.json` or `SFCC_*` env vars) and are never sent to Bridge.\n\n<details>\n<summary><strong>Setup</strong></summary>\n\nThe two diagnostic tools (`sfcc_setup_status`, `check_permissions`) are always available. The seven read tools must be enabled with a profile (step 3).\n\n**Prerequisites:** a running SFCC **sandbox** and its hostname, plus an Account Manager API client (`client-id` + `client-secret`).\n\n**1. Set the repo `version` config field** to your SFCC project type \u2014 one of `sfra | pwakit | sitegenesis | storefrontnext | hybrid`. The call-time gate reads this; a non-SFCC value blocks every SFCC tool except `sfcc_setup_status`. Set it via your normal config path, the `config_field` MCP tool (operation `update`, field `version`), or the `/teach-bridge` skill.\n\n**2. Provide SFCC credentials.** Create a `dw.json` in your project root:\n\n```json\n{\n "hostname": "zzzz-001.sandbox.us01.dx.commercecloud.salesforce.com",\n "client-id": "<account-manager-client-id>",\n "client-secret": "<account-manager-client-secret>"\n}\n```\n\nAccepted key spellings: `hostname`/`host`, `client-id`/`clientId`/`client_id`, `client-secret`/`clientSecret`/`client_secret`. Prefer a single config \u2014 a multi-entry `configs[]` array forces you to pass an explicit `instance` on every call. `dw.json` is auto-added to git exclude and must never be committed. Alternatively, export `SFCC_HOSTNAME` / `SFCC_CLIENT_ID` / `SFCC_CLIENT_SECRET` in the MCP server environment.\n\n**3. Enable the read tools.** Add `sfcc` to `BRIDGE_MCP_PROFILE` in the MCP server `env` block (it is comma-separated; `full` also works), then **restart the MCP client**:\n\n```json\n"env": { "BRIDGE_MCP_PROFILE": "sfcc" }\n```\n\nWithout this, only the diagnostic tools are registered.\n\n**4. Verify and grant OCAPI access.** Ask your agent to run `sfcc_setup_status` (expect all checks \u2713), then `check_permissions`. If it reports HTTP 401/403, it prints the exact OCAPI Settings JSON to paste in Business Manager (**Administration \u2192 Site Development \u2192 Open Commerce API Settings \u2192 Data API** tab); save it there and re-run. Restart the MCP client after any credential or env change \u2014 a running session does not pick them up.\n\n</details>\n\n### Tools\n\nAll SFCC tools are read-only and target a developer sandbox. Oversized responses are automatically saved to `BAPI_DOCS_DIR/sfcc/` and previewed inline.\n\n**Diagnostics** (always available, no profile needed)\n- `sfcc_setup_status` \u2014 report on every prerequisite: Bridge API key, repo name, `version` config, `dw.json` presence/uniqueness, and AM token acquisition.\n- `check_permissions` \u2014 probe OCAPI access via `GET /system_object_definitions`; on 401/403, print the exact OCAPI Settings JSON to paste in Business Manager (split read-only vs. write/import grants).\n\n**System object model** (needs the `sfcc` profile)\n- `system_object_list` \u2014 list system object types (Product, Order, Customer, \u2026).\n- `system_object_get` \u2014 fetch one type\'s definition, optionally with its full attribute definitions (`expand_attribute_definitions`).\n- `system_object_attribute_search` \u2014 search a type\'s attribute definitions; prefer this over a full dump when hunting a specific `c_` custom attribute.\n\n**Custom object definitions** (needs the `sfcc` profile)\n- `custom_object_definition_list` \u2014 list custom object type definitions.\n- `custom_object_definition_get` \u2014 fetch an existing custom type with its key definition and attribute definitions/groups. Read-only \u2014 creating a custom object *type* isn\'t possible via OCAPI; that\'s a future v2 metadata-import capability.\n\n**Site preferences** (needs the `sfcc` profile; sandbox only)\n- `site_preference_get` \u2014 read a preference group\'s effective preferences.\n- `site_preference_search` \u2014 search/filter preferences within a group.\n\n## CLI Subcommands\n\nBeyond `--init` / `--upgrade`, the package ships operational subcommands of the **single `bridge-api-mcp-server` bin** (not separate binaries) \u2014 so they travel with the package to every consumer. See [Usage Documentation \u2192 Start Tickets](#tier-1--regularly-useful) for *when* to use `start-tickets`; this section is the full CLI reference.\n\n### `start-tickets`\n\nSpawns one Worktrunk worktree + selected-agent session per Jira ticket and backs the `/start-tickets` slash command. The agent defaults to **Claude Code** (`claude`) and is configurable via `--agent`.\n\n```\nnpx -y @bridge_gpt/mcp-server start-tickets [flags] KEY [KEY ...]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--agent claude\\|cursor-agent` | `claude` | Agent command to launch in each worktree |\n| `--terminal terminal\\|iterm` | auto-detect via `$TERM_PROGRAM` | Override the macOS terminal app (honored on macOS only) |\n| `--dry-run` | off | Print intended actions; create no worktrees, open no tabs (any OS) |\n| `--branch KEY=BRANCH` | `feature/<KEY>` | Use a custom branch for that ticket (repeatable) |\n| `--base-branch <BRANCH>` | `main` | Cut new worktrees from `<BRANCH>` and refresh `origin/<BRANCH>` instead of `main` |\n| `--no-refresh-main` | off (the configured base branch is refreshed) | Skip refresh of the configured base branch (default `main`). Historical flag name preserved for backward compatibility \u2014 despite the name, it now skips refresh of whatever `--base-branch` resolves to. |\n| `--max-parallel N` | `3` | Max worktrees created concurrently |\n| `--conductor` | off | Opt into the Conductor system (per-worker `BAPI_CONDUCTOR_*` env + Claude hook injection, a supervisor peer tab, and the `check_messages` message-relay prompt). **Default off** \u2014 a plain run spawns `cd <worktree> && <agent> \'/implement-ticket <KEY>\'`. |\n| `-h`, `--help` | \u2014 | Show usage |\n\nEach `KEY` must match `[A-Z]+-[0-9]+` (e.g., `BAPI-248`). The CLI creates/switches each worktree up front (throttled by `--max-parallel`), then opens one tab/session per successful worktree running the selected agent\'s `\'/implement-ticket <KEY>\'` \u2014 `claude \'/implement-ticket <KEY>\'` by default, or `cursor-agent \'/implement-ticket <KEY>\'` with `--agent cursor-agent`. The `/implement-ticket <KEY>` prompt is unchanged for both agents. To launch Cursor Agent instead of Claude Code:\n\n```\nnpx -y @bridge_gpt/mcp-server start-tickets --agent cursor-agent BAPI-248\n```\n\n**Difficulty-based model routing.** Before launching each agent, the CLI selects an implementation **model tier** from the ticket\'s `difficulty` (1-2 \u2192 cheap, 3-5 \u2192 basic, 6+ \u2192 premium) and injects it as a `--model` flag at the spawn boundary. The Python backend returns only the coarse tier (`GET /jira/tickets/{KEY}/model-tier`, computing + caching difficulty on demand); this CLI alone maps a tier to the agent-specific alias (`claude`: `haiku`/`sonnet`/`opus`; `cursor-agent`: version-suffixed strings validated against `cursor-agent --list-models`). It is gated per repo by `difficulty_model_routing_enabled` (default **ON**) with an optional `difficulty_model_tier_overrides` JSON map (tier \u2192 alias). Routing is **fail-open**: missing credentials, an evaluation failure/timeout, a backend `fallback`, an invalid/unavailable alias, an unadvertised Cursor model, or an agent without `--model` support all omit `--model` (the agent uses its default) and surface a per-ticket warning rather than failing the spawn. `--dry-run` does **not** fetch tiers or inject `--model`.\n\n**Conductor observability (opt-in via `--conductor`, BAPI-394).** Conductor is **off by default**. With `--conductor`, a run mints a conductor `run_id`, emits events into the local ledger (`~/.config/bridge/events.db`), opens a supervisor peer tab, and (for Claude Code) injects a secret-free lifecycle hook into each worktree; it also sets `BRIDGE_MCP_PROFILE=conductor` so workers get the conductor MCP tools. Observability is best-effort \u2014 a conductor failure never blocks a spawn, and `--dry-run` performs no conductor side effects. (Epic-tick dispatch always enables conductor internally.) See **[CONDUCTOR.md](./CONDUCTOR.md)** for the full observability, git-hook, done-gate, and auto-merge reference.\n\n**Cross-platform spawning.** The CLI routes spawning per platform; `--dry-run` previews the platform-correct command form on any OS. An unsupported `process.platform` (not `darwin`/`win32`/`linux`) fails fast with a clear "unsupported platform" message.\n\n- **macOS** \u2014 opens a Terminal.app or iTerm tab via `osascript`.\n- **Windows** \u2014 creates worktrees with **`git-wt`** (Worktrunk\'s winget alias) and opens a tab via **Windows Terminal (`wt.exe new-tab`)**, falling back to **`Start-Process powershell.exe`** when Windows Terminal is absent. Requires **Git for Windows / Git Bash** (Worktrunk runs its `pre-start` / `post-start` hooks via Git Bash). The Worktrunk binary (`git-wt`) and the tab launcher (`wt.exe`) are resolved independently and never conflated.\n- **Linux** \u2014 creates one detached **tmux** session per ticket (pane kept open after the agent exits); attach with `tmux attach -t <session>`. A missing `tmux` produces a clear, actionable error.\n\nPer-OS prerequisites: macOS `wt`, `git`, `osascript`; Windows `git-wt`, Git for Windows / Git Bash, Windows Terminal or PowerShell; Linux `wt`, `git`, `tmux`. Set `BAPI_WORKTRUNK_BIN` to override the Worktrunk executable name/path for nonstandard installs (`doctor` honors it too). The read-only `doctor` subcommand (below) additionally surfaces a missing `uv` \u2014 Worktrunk\'s `pre-start` hook runs `uv`, but live preflight does not check it \u2014 and the selected agent\'s command; run `doctor --agent cursor-agent` to also check `cursor-agent` (it prints `cursor-agent login` as an informational auth reminder).\n\n### `doctor`\n\nThe package also ships a strictly **read-only** `doctor` subcommand that diagnoses the `start-tickets` prerequisites for the current OS without changing anything:\n\n```\nnpx -y @bridge_gpt/mcp-server doctor [--agent <name>]\n```\n\nIt is **read-only**: it never installs anything, modifies your system, adds an npm `postinstall`, spawns a terminal, or starts the MCP server, and there is no `--fix`. For each prerequisite it prints found/missing and, when missing, the exact per-OS install command **as a manual instruction you run yourself**. The checked set is the `start-tickets` preflight prerequisites **plus `uv`** **plus the selected agent\'s command** (`claude` by default, or `cursor-agent` with `--agent cursor-agent`). The Worktrunk binary is probed via the resolved name (honoring `BAPI_WORKTRUNK_BIN`), not a hard-coded one. **Exit code:** `0` when all required prerequisites are present, non-zero when any is missing or the platform is unsupported. A failing `start-tickets` preflight now hints you to run `doctor` for an actionable diagnostics report.\n\n### Conductor (epic & multi-agent orchestration)\n\nConductor is an **opt-in, off-by-default** layer for epic supervision, inter-agent messaging, done-gate evaluation, local git-hook event producers, and conditional auto-merge. Its full reference \u2014 `conductor install-git-hooks`, the `conductor_done_gate` and `conductor_auto_merge_enabled` config fields, and the observability stream \u2014 lives in **[CONDUCTOR.md](./CONDUCTOR.md)**.\n\n## Custom Pipelines\n\nYou can create your own pipelines by adding JSON files to `.bridge/pipelines/`. Running `--init` scaffolds this directory with a `README.md` and an example pipeline to get you started.\n\nThe easiest way to write a custom pipeline is to describe what you want to automate to your AI coding agent and have it draft the JSON for you. The schema is straightforward, and agents like Claude Code understand it well \u2014 just describe the steps you want, and the agent will produce a working pipeline file.\n\n**What you can build:**\n\n- Any sequence of Bridge MCP tool calls and free-form agent tasks\n- Parameterized workflows using variables (e.g., `{ticket_key}`)\n- Approval gates that pause for user confirmation before sensitive steps\n- Per-step error handling \u2014 halt immediately or warn and continue\n\n**Ideas for custom pipelines:**\n\n- A standup pipeline that fetches your open tickets and summarizes their status\n- A ticket triage pipeline that runs critiques on a batch of new tickets\n- A pre-merge checklist that runs tests, checks linting, and posts a summary comment\n\n**Step types:**\n\n| Type | What it does |\n|---|---|\n| `mcp_call` | Calls an MCP tool with the given params |\n| `agent_task` | Gives the AI a free-form instruction (inline or from a file in `.bridge/instructions/`) |\n\nVariables are declared in the `variables` array and referenced as `{variable_name}` in params and instructions. Each step supports `on_error: "halt"` (default) or `"warn_and_continue"`, and `requires_approval: true` to pause before execution.\n\n**System variables:**\n\nTwo variables are automatically available in every pipeline without declaring them:\n\n| Variable | What it does |\n|---|---|\n| `{provider}` | Routes AI generation to a specific LLM provider (`openai`, `anthropic`, or `gemini`). Pass it through to any `request_*` tool param to control which provider handles that step. Omit it (or leave it empty) to use the project default. |\n| `{second_opinion}` | Runs AI generation through a different provider than the default, acting as a cross-check. Set to `"auto"` to let Bridge pick the second provider automatically. When set, it takes precedence over `{provider}`. Use this when you want two independent AI perspectives on the same task \u2014 for example, running clarifying questions and a critique twice (once with each provider) produces better results than a single pass. |\n\nSee `.bridge/pipelines/README.md` for the full schema reference.\n\nIf a custom pipeline has the same key as a built-in pipeline, the custom version takes precedence (a warning is logged at startup).\n\n## Environment Variables\n\n| Variable | Required | Default | Description |\n|---|---|---|---|\n| `BAPI_BASE_URL` | Yes | `https://bridgegpt-api.com` | Bridge API base URL |\n| `BAPI_REPO_NAME` | Yes | _(none)_ | Jira project/repository identifier configured in Bridge API |\n| `BAPI_API_KEY` | Yes | _(none)_ | API key obtained from the Bridge API setup UI |\n| `BAPI_PROJECT_ROOT` | No | _(auto-set by --init)_ | Absolute path to project root. Anchors `BAPI_DOCS_DIR` and `BAPI_PIPELINES_DIR` resolution |\n| `BAPI_DOCS_DIR` | No | `docs/tmp` | Local directory for saving plans, critiques, and research reports |\n| `BAPI_PIPELINES_DIR` | No | `.bridge/pipelines` | Directory for user-defined custom pipeline JSON files |\n| `BAPI_WORKTRUNK_BIN` | No | `wt` (`git-wt` on Windows) | Override the Worktrunk executable name/path used by `start-tickets` for nonstandard installs |\n| `BAPI_TMUX_SESSION` | No | `bridge-start-tickets` | Override the tmux session-name prefix used by `start-tickets` on Linux |\n| `BAPI_MCP_UPGRADE_ADVICE_ENABLED` | No | _(enabled)_ | MCP-local opt-out for proactively surfacing upgrade advice in pipeline recipe preambles. Set to `false`/`0`/`no`/`off`/`disabled` to suppress. Disabling it does **not** change the `/jira/ping` response or server-side upgrade computation \u2014 it only gates the recipe-preamble convention |\n| `CURSOR_API_KEY` | No | _(none)_ | API key used to authenticate `cursor-agent` when launching `start-tickets`/scheduled runs with `--agent cursor-agent`. Not needed for the default Claude Code agent |\n| `BRIDGE_MCP_PROFILE` | No | `core` | Startup-time tool registration profile \u2014 a **comma-separated** list of groups controlling which tool groups are registered when the server starts. `core` is always implicitly included. Valid groups: `core` (default \u2014 normal coding tools only), `conductor` (+ 8 conductor/event/supervisor tools), `pipeline-authoring` (+ 5 pipeline run/admin tools \u2014 `get_pipeline_recipe` is NOT gated; it stays in `core` because the recipe-driven slash commands depend on it), `sfcc` (+ the 7 heavy SFCC read tools \u2014 see [Salesforce B2C Commerce (SFCC) Tools](#salesforce-b2c-commerce-sfcc-tools); the `sfcc_setup_status`/`check_permissions` diagnostics are always registered regardless), and `full` (shortcut that expands to every group). Example: `sfcc,conductor`. Unknown, blank, or malformed tokens are dropped (falling back to `core`). Dynamic mid-session switching via `tools/list_changed` is unsupported \u2014 groups are resolved once at process startup. Conductor/epic sessions launched via `start-tickets --conductor` automatically inject `BRIDGE_MCP_PROFILE=conductor` at the spawn boundary; a normal `start-tickets` run stays on `core`. |\n\n## Worktree credentials and the `mcp-invoke` shim\n\nWhen `start-tickets` creates a git worktree, it provisions a Bridge API MCP\nregistration into that worktree so Claude Code (`.mcp.json`) and Cursor\n(`.cursor/mcp.json`) can reach the server immediately. These registrations are\n**secret-free**: they contain no `env` block and no API key. Instead they point\nat an internal subcommand of the published single CLI bin, `mcp-invoke`:\n\n```bash\nnpx -y @bridge_gpt/mcp-server@<VERSION> mcp-invoke --target bapi --project-root <ABS_WORKTREE_PATH>\n```\n\n`mcp-invoke` is not a separate binary \u2014 it is a positional subcommand of\n`bridge-api-mcp-server`. It resolves the repo identity from the absolute\n`--project-root` (the committed `.bridge/config` manifest, falling back to the\ngit common dir), resolves credentials from the home-directory credential store,\nand then spawns the real MCP server with that environment.\n\n### Credential store\n\nCredentials live outside the repository, keyed by `bapi:<repo_name>`:\n\n```json\n{\n "bapi:<repo_name>": {\n "BAPI_API_KEY": "..."\n }\n}\n```\n\nResolution order:\n\n1. `BAPI_API_KEY` in the parent environment (overrides the file entirely).\n2. `$XDG_CONFIG_HOME/bridge/credentials.json`, else `~/.config/bridge/credentials.json`.\n3. `~/.bridge/credentials.json` (only when the primary path is absent).\n\nOn POSIX systems, lock the file down so only you can read it:\n\n```bash\nchmod 600 ~/.config/bridge/credentials.json\n```\n\n`mcp-invoke` warns (but continues) if the file is group/world-readable, and it\nnever creates or initializes the credential file for you.\n\n### Populating the credential store\n\nThe same store also backs the shell-spawned `start-tickets` CLI (its\ndifficulty\u2192model routing runs in a Bash process that cannot see an `env` block in\n`.mcp.json` / `.cursor/mcp.json`), so the store must hold the key for routing to\nwork. Two supported paths write it for you:\n\n- **Install-time upsert.** `/install-bridge`\'s final stage persists the validated\n routing credential into `~/.config/bridge/credentials.json` (target\n `bapi:<repo>`) via the `persist_routing_credential` MCP tool \u2014 the tool resolves\n the key inside the MCP process and writes the store, so no secret crosses the\n wire.\n- **One-shot migration.** If a key currently lives only in `.mcp.json` /\n `.cursor/mcp.json`, migrate it into the user-scoped store with the consent-gated\n command (a compatibility aid, not a live fallback):\n\n ```bash\n npx -y @bridge_gpt/mcp-server credentials migrate-agent-config [--write-credentials]\n ```\n\n Run it without `--write-credentials` to preview; add the flag to write the store.\n\n## Reference\n\nThe full surface, for when you need the complete enumeration. Day-to-day, use [Usage Documentation](#usage-documentation) instead \u2014 you don\'t call MCP tools directly; you ask your AI assistant to perform a task, or compose tools into a pipeline.\n\n### MCP tools\n\nThe server exposes **58 documented tools** (enumerated below). What\'s actually registered in a session depends on `BRIDGE_MCP_PROFILE`: the default `core` profile loads a trimmed subset, and the conductor/pipeline-authoring/SFCC tools are added only under their respective profiles (see [Environment Variables](#environment-variables)). Async AI tools follow a request/get pattern: call the `request_*` tool to kick off generation, then the matching `get_*` tool to retrieve the result (or pass `wait_for_result: true` to poll automatically).\n\n- **Connectivity & identity** \u2014 `ping`, `get_my_role`, `get_docs_dir`\n- **Jira tickets** \u2014 `get_tickets`, `get_ticket`, `create_ticket`, `update_ticket_description`, `add_comment`, `get_comments`\n- **Attachments** \u2014 `attachment` (operations: `upload`, `download`, `list`)\n- **AI generation (request/get)** \u2014 `request_plan_generation`/`get_plan`, `request_architecture`/`get_architecture`, `create_doc`/`get_doc` (design docs by `doc_type`: tdd/fsd/prd), `request_prd`/`get_prd`, `request_clarifying_questions`/`get_clarifying_questions`, `request_ticket_critique`/`get_ticket_critique`, `request_ticket_review`, `request_reimplement_context`/`get_reimplement_context`, `request_brainstorm`/`get_brainstorm`, `request_deep_research`/`get_deep_research`\n- **Other AI** \u2014 `second_opinion`, `generate_image`, `generate_decision_page`\n- **Ticket lifecycle** \u2014 `track_ticket`, `update_ticket_state`, `get_ticket_state`\n- **Jira status** \u2014 `get_jira_transitions`, `update_jira_status`, `resolve_target_status`\n- **Repository & CI** \u2014 `parse_repository`, `get_parse_status`, `regenerate_directory_map`, `create_pull_request`, `resolve_ci_checks`, `poll_ci_checks`\n- **Pipelines & automation** \u2014 `list_pipelines`, `get_pipeline_recipe`, `run_pipeline`, `resume_pipeline`, `list_pipeline_runs`, `delete_pipeline_run`, `run_full_automation`, `resume_full_automation`\n- **Config** \u2014 `get_project_standards`, `config_field` (operations: `get`, `update`, `list`)\n\n### Bundled pipelines\n\nPipelines are declarative, multi-step workflows your AI agent executes step-by-step \u2014 each a JSON recipe chaining MCP tool calls and free-form agent tasks, with variable substitution, per-step error handling, and optional approval gates. You can also write your own (see [Custom Pipelines](#custom-pipelines)).\n\n| Pipeline | Description | Invoke with |\n|---|---|---|\n| `implement-ticket` | Generate a plan, execute the implementation, commit, open a PR, and monitor CI | `/implement-ticket PROJ-123` |\n| `review-ticket` | Full ticket quality review: initial clarifying questions + critique, automatic second-opinion pass (default), then evaluation and decision capture. Pass `--rounds=1` to skip the second-opinion step (`--rounds=2` is the default). | `/review-ticket PROJ-123` |\n| `idea-to-ticket` | Turn an idea into a Jira Task/Spike (or Epic + children) with research, dedup, and critique | `/idea-to-ticket "<idea>"` |\n| `plan-epic` | Decompose an epic into sub-tasks with a structured exploration doc for each | `/plan-epic "<epic>"` |\n| `full-automation` | Chain: idea \u2192 ticket(s) \u2192 review each \u2192 spawn worktrees to implement | `/full-automation "<idea>"` |\n| `pr-ticket` | Commit changes and open a pull request | `/create-pr PROJ-123` |\n| `check-ci-ticket` | Commit, open a PR, then monitor CI checks until they pass or fail | `/check-ci PROJ-123` |\n| `learn-repository` | Analyze codebase architecture, testing, review, and documentation standards, then upload to Bridge | `/learn-repository` |\n\n### Pipeline response envelope\n\n`run_pipeline`, `resume_pipeline`, and `list_pipeline_runs` share a unified envelope keyed on `status`:\n\n- `completed` \u2014 terminal success; `results` holds per-step output.\n- `needs_agent_task` \u2014 the orchestrator paused. Read `instruction`, perform the task, then call `resume_pipeline` with `pipeline_run_id` and a string `agent_result`.\n- `failed` \u2014 terminal failure. `error_code` is one of `VALIDATION`, `NOT_FOUND`, `EXPIRED`, `REPO_MISMATCH`, `TOOL_ERROR`.\n\nPaused runs auto-expire after an idle TTL (default 24 hours; override with `ttl_seconds`). The TTL is reset on every state transition. List output is metadata-only \u2014 it never includes resolved recipes, params, instructions, results, or agent outputs.\n';
|
|
12707
13071
|
|
|
12708
13072
|
// src/update-check.ts
|
|
12709
13073
|
init_version_generated();
|
|
@@ -13193,6 +13557,18 @@ async function ensureGitignored(cwd, filePath, deps) {
|
|
|
13193
13557
|
if (hasExactLine(content, entry)) return;
|
|
13194
13558
|
await deps.writeFile(gitignorePath, appendLine(content, entry));
|
|
13195
13559
|
}
|
|
13560
|
+
async function ensureGitInfoExcluded(worktreeRoot, relativePath, deps) {
|
|
13561
|
+
const infoDir = path4.join(worktreeRoot, ".git", "info");
|
|
13562
|
+
const excludePath = path4.join(infoDir, "exclude");
|
|
13563
|
+
let content = "";
|
|
13564
|
+
try {
|
|
13565
|
+
content = await deps.readFile(excludePath);
|
|
13566
|
+
} catch {
|
|
13567
|
+
}
|
|
13568
|
+
if (hasExactLine(content, relativePath)) return;
|
|
13569
|
+
await deps.mkdir(infoDir, { recursive: true });
|
|
13570
|
+
await deps.writeFile(excludePath, appendLine(content, relativePath));
|
|
13571
|
+
}
|
|
13196
13572
|
|
|
13197
13573
|
// src/init.ts
|
|
13198
13574
|
function buildBridgeApiEntry(cwd) {
|
|
@@ -13236,6 +13612,44 @@ function buildBridgeConfigManifest(repoName) {
|
|
|
13236
13612
|
""
|
|
13237
13613
|
].join("\n");
|
|
13238
13614
|
}
|
|
13615
|
+
async function mergeBridgeApiProfileToken(cwd, token) {
|
|
13616
|
+
const targets = [
|
|
13617
|
+
{ path: ".mcp.json", topLevelKey: "mcpServers" },
|
|
13618
|
+
{ path: ".vscode/mcp.json", topLevelKey: "servers" },
|
|
13619
|
+
{ path: ".cursor/mcp.json", topLevelKey: "mcpServers" }
|
|
13620
|
+
];
|
|
13621
|
+
let lastProfile = "";
|
|
13622
|
+
for (const target of targets) {
|
|
13623
|
+
const fullPath = path5.join(cwd, target.path);
|
|
13624
|
+
let raw;
|
|
13625
|
+
try {
|
|
13626
|
+
raw = await readFile3(fullPath, "utf-8");
|
|
13627
|
+
} catch {
|
|
13628
|
+
continue;
|
|
13629
|
+
}
|
|
13630
|
+
let parsed;
|
|
13631
|
+
try {
|
|
13632
|
+
parsed = JSON.parse(raw);
|
|
13633
|
+
} catch {
|
|
13634
|
+
continue;
|
|
13635
|
+
}
|
|
13636
|
+
const topLevel = parsed[target.topLevelKey];
|
|
13637
|
+
if (!topLevel || !topLevel["bridge-api"]) {
|
|
13638
|
+
continue;
|
|
13639
|
+
}
|
|
13640
|
+
const entry = topLevel["bridge-api"];
|
|
13641
|
+
if (!entry.env) entry.env = {};
|
|
13642
|
+
const existing = entry.env.BRIDGE_MCP_PROFILE;
|
|
13643
|
+
const existingTokens = existing ? existing.split(",").map((t) => t.trim()).filter((t) => t.length > 0) : [];
|
|
13644
|
+
const tokenSet = new Set(existingTokens);
|
|
13645
|
+
tokenSet.add(token);
|
|
13646
|
+
const merged = Array.from(tokenSet).join(",");
|
|
13647
|
+
entry.env.BRIDGE_MCP_PROFILE = merged;
|
|
13648
|
+
await writeFile2(fullPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
|
|
13649
|
+
lastProfile = merged;
|
|
13650
|
+
}
|
|
13651
|
+
return lastProfile;
|
|
13652
|
+
}
|
|
13239
13653
|
async function ensureGitignored2(cwd, filePath) {
|
|
13240
13654
|
await ensureGitignored(cwd, filePath, {
|
|
13241
13655
|
readFile: (p) => readFile3(p, "utf-8"),
|
|
@@ -13621,6 +14035,7 @@ init_start_tickets();
|
|
|
13621
14035
|
init_version_generated();
|
|
13622
14036
|
init_agent_registry();
|
|
13623
14037
|
init_start_tickets_prereqs();
|
|
14038
|
+
init_mcp_profile();
|
|
13624
14039
|
import { readFile as readFile6, stat as stat4 } from "fs/promises";
|
|
13625
14040
|
import { spawn } from "child_process";
|
|
13626
14041
|
import os5 from "os";
|
|
@@ -13712,10 +14127,12 @@ async function collectDoctorResults(deps, agentName) {
|
|
|
13712
14127
|
return { ok: true, results };
|
|
13713
14128
|
}
|
|
13714
14129
|
function formatDoctorReport(platform, agent, collection) {
|
|
14130
|
+
const activeGroups = Array.from(resolveProfiles(process.env.BRIDGE_MCP_PROFILE)).join(", ");
|
|
13715
14131
|
const lines = [
|
|
13716
14132
|
"start-tickets doctor (read-only diagnostics)",
|
|
13717
14133
|
`Platform: ${platform}`,
|
|
13718
14134
|
`Selected agent: ${agent.name} (command: ${agent.command})`,
|
|
14135
|
+
`Active MCP Groups: \`${activeGroups}\``,
|
|
13719
14136
|
""
|
|
13720
14137
|
];
|
|
13721
14138
|
if (!collection.ok) {
|
|
@@ -14590,8 +15007,8 @@ var outputFormat = {
|
|
|
14590
15007
|
cwd: dir,
|
|
14591
15008
|
outputFormat: "text"
|
|
14592
15009
|
});
|
|
14593
|
-
const
|
|
14594
|
-
if (
|
|
15010
|
+
const textResult4 = assertMarkers(textRun, [marker], "text output-format did not emit the marker");
|
|
15011
|
+
if (textResult4.status !== "pass") return textResult4;
|
|
14595
15012
|
const jsonRun = await ctx.runHeadless({
|
|
14596
15013
|
prompt: `Do not use any tools. Output exactly the token ${marker}.`,
|
|
14597
15014
|
cwd: dir,
|
|
@@ -15587,10 +16004,10 @@ import readline2 from "readline";
|
|
|
15587
16004
|
init_credential_store();
|
|
15588
16005
|
init_start_tickets_repo();
|
|
15589
16006
|
import path18 from "path";
|
|
15590
|
-
async function readAgentMcpConfigIfPresent(filePath,
|
|
16007
|
+
async function readAgentMcpConfigIfPresent(filePath, readFile12) {
|
|
15591
16008
|
let raw;
|
|
15592
16009
|
try {
|
|
15593
|
-
raw = await
|
|
16010
|
+
raw = await readFile12(filePath);
|
|
15594
16011
|
} catch (err) {
|
|
15595
16012
|
const code = err && typeof err === "object" ? err.code : void 0;
|
|
15596
16013
|
if (code === "ENOENT") {
|
|
@@ -16341,110 +16758,32 @@ function evaluateDoneGate(config, binding, snapshot, evaluatedAtIso, reviewSnaps
|
|
|
16341
16758
|
// src/conductor/pr-review-producer.ts
|
|
16342
16759
|
init_git_ci_types();
|
|
16343
16760
|
init_bridge_api_client();
|
|
16344
|
-
|
|
16345
|
-
|
|
16346
|
-
|
|
16347
|
-
|
|
16348
|
-
|
|
16349
|
-
|
|
16350
|
-
|
|
16351
|
-
|
|
16352
|
-
|
|
16353
|
-
|
|
16354
|
-
|
|
16355
|
-
|
|
16356
|
-
|
|
16357
|
-
|
|
16358
|
-
|
|
16359
|
-
|
|
16360
|
-
|
|
16361
|
-
|
|
16362
|
-
|
|
16363
|
-
|
|
16364
|
-
|
|
16365
|
-
|
|
16366
|
-
|
|
16367
|
-
|
|
16368
|
-
|
|
16369
|
-
|
|
16370
|
-
}
|
|
16371
|
-
var LEDGER_SCAN_PAGE_LIMIT = 500;
|
|
16372
|
-
var LEDGER_SCAN_MAX_PAGES = 200;
|
|
16373
|
-
async function eventAlreadyExists(dedupeKey, deps = {}) {
|
|
16374
|
-
const pollEvents = deps.pollEvents ?? ((options) => pollConductorEvents(options));
|
|
16375
|
-
let sinceSeq = 1;
|
|
16376
|
-
for (let page = 0; page < LEDGER_SCAN_MAX_PAGES; page += 1) {
|
|
16377
|
-
let result;
|
|
16378
|
-
try {
|
|
16379
|
-
result = await pollEvents({ since_seq: sinceSeq, data_mode: "full", limit: LEDGER_SCAN_PAGE_LIMIT });
|
|
16380
|
-
} catch {
|
|
16381
|
-
return false;
|
|
16382
|
-
}
|
|
16383
|
-
for (const event of result.events) {
|
|
16384
|
-
if (!event || typeof event !== "object") continue;
|
|
16385
|
-
const data = event.data;
|
|
16386
|
-
if (data && typeof data === "object") {
|
|
16387
|
-
const details = data.details;
|
|
16388
|
-
if (details && typeof details === "object" && details.dedupe_key === dedupeKey) {
|
|
16389
|
-
return true;
|
|
16390
|
-
}
|
|
16391
|
-
}
|
|
16392
|
-
}
|
|
16393
|
-
if (result.count === 0 || result.next_seq <= sinceSeq) break;
|
|
16394
|
-
sinceSeq = result.next_seq;
|
|
16395
|
-
}
|
|
16396
|
-
return false;
|
|
16397
|
-
}
|
|
16398
|
-
async function emitConductorEventIfNew(input, dimensions, deps = {}) {
|
|
16399
|
-
const emitEvent = deps.emitEvent ?? emitConductorEvent;
|
|
16400
|
-
const dedupeKey = makeProducerDedupeKey(dimensions);
|
|
16401
|
-
if (await eventAlreadyExists(dedupeKey, deps)) {
|
|
16402
|
-
return { emitted: false, reason: "duplicate" };
|
|
16403
|
-
}
|
|
16404
|
-
const eventId = makeStableProducerEventId(dedupeKey);
|
|
16405
|
-
const existingData = input.data ?? {};
|
|
16406
|
-
const existingDetails = existingData.details && typeof existingData.details === "object" && !Array.isArray(existingData.details) ? existingData.details : {};
|
|
16407
|
-
const data = {
|
|
16408
|
-
...existingData,
|
|
16409
|
-
details: { ...existingDetails, dedupe_key: dedupeKey }
|
|
16410
|
-
};
|
|
16411
|
-
try {
|
|
16412
|
-
await emitEvent({ ...input, id: eventId, data });
|
|
16413
|
-
return { emitted: true, event_id: eventId };
|
|
16414
|
-
} catch (error) {
|
|
16415
|
-
if (isDuplicateConstraintError2(error)) {
|
|
16416
|
-
return { emitted: false, reason: "duplicate" };
|
|
16417
|
-
}
|
|
16418
|
-
throw error;
|
|
16419
|
-
}
|
|
16420
|
-
}
|
|
16421
|
-
|
|
16422
|
-
// src/conductor/pr-review-producer.ts
|
|
16423
|
-
var REVIEW_PRODUCER_OBSERVED_VIA = "pr-review-producer";
|
|
16424
|
-
function buildReviewObservationEventInput(binding, snapshot, eventType, reason, runId = null, workerId = null) {
|
|
16425
|
-
return {
|
|
16426
|
-
source: "review",
|
|
16427
|
-
type: eventType,
|
|
16428
|
-
subject: binding.subject,
|
|
16429
|
-
run_id: runId,
|
|
16430
|
-
worker_id: workerId,
|
|
16431
|
-
producer: GIT_CI_PRODUCER,
|
|
16432
|
-
observed_via: REVIEW_PRODUCER_OBSERVED_VIA,
|
|
16433
|
-
data: {
|
|
16434
|
-
summary: eventType === REVIEW_PASSED ? `Review passed for ${binding.subject}` : `Review changes requested for ${binding.subject}`,
|
|
16435
|
-
status: eventType === REVIEW_PASSED ? "passed" : "changes_requested",
|
|
16436
|
-
details: {
|
|
16437
|
-
repo: binding.repo,
|
|
16438
|
-
pr_number: binding.pr_number,
|
|
16439
|
-
head_sha: binding.head_sha,
|
|
16440
|
-
review_decision: snapshot.review_decision,
|
|
16441
|
-
approvals: snapshot.approvals,
|
|
16442
|
-
sticky_verdict: snapshot.sticky_verdict,
|
|
16443
|
-
review_state_hash: snapshot.review_state_hash,
|
|
16444
|
-
reason
|
|
16445
|
-
}
|
|
16446
|
-
}
|
|
16447
|
-
};
|
|
16761
|
+
init_producer_ledger();
|
|
16762
|
+
var REVIEW_PRODUCER_OBSERVED_VIA = "pr-review-producer";
|
|
16763
|
+
function buildReviewObservationEventInput(binding, snapshot, eventType, reason, runId = null, workerId = null) {
|
|
16764
|
+
return {
|
|
16765
|
+
source: "review",
|
|
16766
|
+
type: eventType,
|
|
16767
|
+
subject: binding.subject,
|
|
16768
|
+
run_id: runId,
|
|
16769
|
+
worker_id: workerId,
|
|
16770
|
+
producer: GIT_CI_PRODUCER,
|
|
16771
|
+
observed_via: REVIEW_PRODUCER_OBSERVED_VIA,
|
|
16772
|
+
data: {
|
|
16773
|
+
summary: eventType === REVIEW_PASSED ? `Review passed for ${binding.subject}` : `Review changes requested for ${binding.subject}`,
|
|
16774
|
+
status: eventType === REVIEW_PASSED ? "passed" : "changes_requested",
|
|
16775
|
+
details: {
|
|
16776
|
+
repo: binding.repo,
|
|
16777
|
+
pr_number: binding.pr_number,
|
|
16778
|
+
head_sha: binding.head_sha,
|
|
16779
|
+
review_decision: snapshot.review_decision,
|
|
16780
|
+
approvals: snapshot.approvals,
|
|
16781
|
+
sticky_verdict: snapshot.sticky_verdict,
|
|
16782
|
+
review_state_hash: snapshot.review_state_hash,
|
|
16783
|
+
reason
|
|
16784
|
+
}
|
|
16785
|
+
}
|
|
16786
|
+
};
|
|
16448
16787
|
}
|
|
16449
16788
|
async function observeReviewWithResolved(binding, access, gateConfig, deps = {}) {
|
|
16450
16789
|
const fetchStatus = deps.fetchReviewStatus ?? fetchPrReviewStatus;
|
|
@@ -16745,6 +17084,7 @@ function resolvePrHeadBinding(input = {}, deps = {}) {
|
|
|
16745
17084
|
}
|
|
16746
17085
|
|
|
16747
17086
|
// src/conductor/pr-ci-producer.ts
|
|
17087
|
+
init_producer_ledger();
|
|
16748
17088
|
async function _fetchGateConfigDefault(access) {
|
|
16749
17089
|
const setup = await fetchEffectiveSupervisorSetup(access);
|
|
16750
17090
|
if (setup.source === "none") return void 0;
|
|
@@ -17081,7 +17421,11 @@ async function observePrCiFromPollResponse(commitRef, pollResponse, deps = {}) {
|
|
|
17081
17421
|
rawConfig = void 0;
|
|
17082
17422
|
}
|
|
17083
17423
|
const gateConfig = parseDoneGateConfig(rawConfig);
|
|
17084
|
-
|
|
17424
|
+
const requiresReview = gateConfig.conditions.some((c) => c.type === REVIEW_STATE);
|
|
17425
|
+
if (gateConfig.enabled && gateConfig.valid && requiresReview) {
|
|
17426
|
+
result.reason = "review-gated config: gate.met deferred to wait_for_done_gate (poll path is CI-only)";
|
|
17427
|
+
}
|
|
17428
|
+
if (gateConfig.enabled && gateConfig.valid && !requiresReview) {
|
|
17085
17429
|
const evaluation = evaluateDoneGate(gateConfig, binding, snapshot, now());
|
|
17086
17430
|
if (evaluation.met) {
|
|
17087
17431
|
result.gate_met = true;
|
|
@@ -17457,24 +17801,977 @@ function registerConductorTools(registerTool2) {
|
|
|
17457
17801
|
registerCheckMessagesTool(reg);
|
|
17458
17802
|
}
|
|
17459
17803
|
|
|
17460
|
-
// src/
|
|
17461
|
-
|
|
17462
|
-
|
|
17463
|
-
|
|
17464
|
-
|
|
17465
|
-
|
|
17466
|
-
|
|
17467
|
-
|
|
17468
|
-
|
|
17469
|
-
|
|
17470
|
-
|
|
17471
|
-
|
|
17472
|
-
|
|
17804
|
+
// src/sfcc/register.ts
|
|
17805
|
+
import { z as z6 } from "zod";
|
|
17806
|
+
|
|
17807
|
+
// src/sfcc/config.ts
|
|
17808
|
+
var SFCC_VERSIONS = ["sfra", "pwakit", "sitegenesis", "storefrontnext", "hybrid"];
|
|
17809
|
+
var DEFAULT_OCAPI_VERSION = "v25_6";
|
|
17810
|
+
var AM_HOST = "account.demandware.com";
|
|
17811
|
+
var AM_TOKEN_URL = `https://${AM_HOST}/dwsso/oauth2/access_token`;
|
|
17812
|
+
async function getSfccVersionConfig(buildGetUrl2, getGetHeaders2, repoName) {
|
|
17813
|
+
try {
|
|
17814
|
+
const url = buildGetUrl2("/config-field/version", { repo_name: repoName });
|
|
17815
|
+
const resp = await fetch(url, { headers: await getGetHeaders2() });
|
|
17816
|
+
if (!resp.ok) return null;
|
|
17817
|
+
const body = await resp.json();
|
|
17818
|
+
const value = body.value;
|
|
17819
|
+
if (value === null || value === void 0 || typeof value !== "string") return null;
|
|
17820
|
+
return value;
|
|
17821
|
+
} catch {
|
|
17822
|
+
return null;
|
|
17823
|
+
}
|
|
17824
|
+
}
|
|
17825
|
+
|
|
17826
|
+
// src/sfcc/credentials.ts
|
|
17827
|
+
import { readFile as readFile10, writeFile as writeFile7, mkdir as mkdir7 } from "fs/promises";
|
|
17828
|
+
import path19 from "path";
|
|
17829
|
+
var ENV_HOSTNAME = "SFCC_HOSTNAME";
|
|
17830
|
+
var ENV_CLIENT_ID = "SFCC_CLIENT_ID";
|
|
17831
|
+
var ENV_CLIENT_SECRET = "SFCC_CLIENT_SECRET";
|
|
17832
|
+
var DW_JSON = "dw.json";
|
|
17833
|
+
function safeHostLabel(hostname) {
|
|
17834
|
+
return hostname.split(".")[0] ?? hostname;
|
|
17835
|
+
}
|
|
17836
|
+
async function resolveSfccCredentials(explicitHostname, env = process.env, deps = {}) {
|
|
17837
|
+
if (explicitHostname) {
|
|
17838
|
+
const clientId2 = env[ENV_CLIENT_ID];
|
|
17839
|
+
const clientSecret2 = env[ENV_CLIENT_SECRET];
|
|
17840
|
+
if (!clientId2 || !clientSecret2) {
|
|
17841
|
+
return {
|
|
17842
|
+
ok: false,
|
|
17843
|
+
error: `Explicit instance '${safeHostLabel(explicitHostname)}' provided but ${ENV_CLIENT_ID} and/or ${ENV_CLIENT_SECRET} are not set in environment. Set them and retry.`
|
|
17844
|
+
};
|
|
17845
|
+
}
|
|
17846
|
+
return {
|
|
17847
|
+
ok: true,
|
|
17848
|
+
credentials: {
|
|
17849
|
+
hostname: explicitHostname,
|
|
17850
|
+
clientId: clientId2,
|
|
17851
|
+
clientSecret: clientSecret2,
|
|
17852
|
+
source: `explicit arg + env (${ENV_CLIENT_ID}/${ENV_CLIENT_SECRET})`
|
|
17853
|
+
}
|
|
17854
|
+
};
|
|
17855
|
+
}
|
|
17856
|
+
const envHostname = env[ENV_HOSTNAME];
|
|
17857
|
+
const envClientId = env[ENV_CLIENT_ID];
|
|
17858
|
+
const envClientSecret = env[ENV_CLIENT_SECRET];
|
|
17859
|
+
if (envHostname && envClientId && envClientSecret) {
|
|
17860
|
+
return {
|
|
17861
|
+
ok: true,
|
|
17862
|
+
credentials: {
|
|
17863
|
+
hostname: envHostname,
|
|
17864
|
+
clientId: envClientId,
|
|
17865
|
+
clientSecret: envClientSecret,
|
|
17866
|
+
source: `env (${ENV_HOSTNAME}/${ENV_CLIENT_ID}/${ENV_CLIENT_SECRET})`
|
|
17867
|
+
}
|
|
17868
|
+
};
|
|
17869
|
+
}
|
|
17870
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
17871
|
+
const rf = deps.readFile ?? ((p) => readFile10(p, "utf-8"));
|
|
17872
|
+
const wf = deps.writeFile ?? ((p, data) => writeFile7(p, data, "utf-8"));
|
|
17873
|
+
const mk = deps.mkdir ?? ((p, opts) => mkdir7(p, opts));
|
|
17874
|
+
try {
|
|
17875
|
+
await ensureGitInfoExcluded(cwd, DW_JSON, { readFile: rf, writeFile: wf, mkdir: mk });
|
|
17876
|
+
} catch {
|
|
17877
|
+
}
|
|
17878
|
+
const dwJsonPath = path19.join(cwd, DW_JSON);
|
|
17879
|
+
let dwJson;
|
|
17880
|
+
try {
|
|
17881
|
+
const raw = await rf(dwJsonPath);
|
|
17882
|
+
dwJson = JSON.parse(raw);
|
|
17883
|
+
} catch {
|
|
17884
|
+
return {
|
|
17885
|
+
ok: false,
|
|
17886
|
+
error: "Could not read dw.json. Create a dw.json file in your project root with your SFCC sandbox credentials (hostname, client-id, client-secret)."
|
|
17887
|
+
};
|
|
17888
|
+
}
|
|
17889
|
+
const configs = Array.isArray(dwJson.configs) ? dwJson.configs : null;
|
|
17890
|
+
if (configs && configs.length > 1) {
|
|
17891
|
+
const instances = configs.map((c) => safeHostLabel(String(c.hostname ?? c.host ?? "unknown"))).join(", ");
|
|
17892
|
+
return {
|
|
17893
|
+
ok: false,
|
|
17894
|
+
error: `dw.json contains multiple sandboxes (${instances}). Pass an explicit 'instance' argument to select one: ${instances}.`
|
|
17895
|
+
};
|
|
17896
|
+
}
|
|
17897
|
+
const cfg = configs && configs.length === 1 ? configs[0] : dwJson;
|
|
17898
|
+
const hostname = String(cfg.hostname ?? cfg.host ?? "");
|
|
17899
|
+
const clientId = String(cfg["client-id"] ?? cfg.clientId ?? cfg.client_id ?? "");
|
|
17900
|
+
const clientSecret = String(
|
|
17901
|
+
cfg["client-secret"] ?? cfg.clientSecret ?? cfg.client_secret ?? ""
|
|
17902
|
+
);
|
|
17903
|
+
if (!hostname || !clientId || !clientSecret) {
|
|
17904
|
+
return {
|
|
17905
|
+
ok: false,
|
|
17906
|
+
error: "dw.json is present but missing required fields (hostname, client-id, client-secret). Ensure all three fields are set."
|
|
17907
|
+
};
|
|
17908
|
+
}
|
|
17909
|
+
return {
|
|
17910
|
+
ok: true,
|
|
17911
|
+
credentials: {
|
|
17912
|
+
hostname,
|
|
17913
|
+
clientId,
|
|
17914
|
+
clientSecret,
|
|
17915
|
+
source: `dw.json (instance: ${safeHostLabel(hostname)})`
|
|
17916
|
+
}
|
|
17917
|
+
};
|
|
17473
17918
|
}
|
|
17474
|
-
|
|
17475
|
-
|
|
17919
|
+
|
|
17920
|
+
// src/sfcc/client.ts
|
|
17921
|
+
var tokenMutex = /* @__PURE__ */ new Map();
|
|
17922
|
+
var tokenCache = /* @__PURE__ */ new Map();
|
|
17923
|
+
async function getAmToken(credentials) {
|
|
17924
|
+
const instanceKey = credentials.hostname;
|
|
17925
|
+
const cached = tokenCache.get(instanceKey);
|
|
17926
|
+
if (cached) return cached;
|
|
17927
|
+
const existing = tokenMutex.get(instanceKey);
|
|
17928
|
+
if (existing) return existing;
|
|
17929
|
+
const promise = (async () => {
|
|
17930
|
+
const body = new URLSearchParams({
|
|
17931
|
+
grant_type: "client_credentials",
|
|
17932
|
+
client_id: credentials.clientId,
|
|
17933
|
+
client_secret: credentials.clientSecret
|
|
17934
|
+
});
|
|
17935
|
+
const resp = await fetch(AM_TOKEN_URL, {
|
|
17936
|
+
method: "POST",
|
|
17937
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
17938
|
+
body: body.toString()
|
|
17939
|
+
});
|
|
17940
|
+
if (!resp.ok) {
|
|
17941
|
+
throw new Error(
|
|
17942
|
+
`AM token acquisition failed: HTTP ${resp.status} for instance ${instanceKey.split(".")[0]}`
|
|
17943
|
+
);
|
|
17944
|
+
}
|
|
17945
|
+
const data = await resp.json();
|
|
17946
|
+
const token = typeof data.access_token === "string" ? data.access_token : "";
|
|
17947
|
+
if (!token) {
|
|
17948
|
+
throw new Error(
|
|
17949
|
+
`AM token acquisition returned no access_token for instance ${instanceKey.split(".")[0]}`
|
|
17950
|
+
);
|
|
17951
|
+
}
|
|
17952
|
+
tokenCache.set(instanceKey, token);
|
|
17953
|
+
return token;
|
|
17954
|
+
})();
|
|
17955
|
+
tokenMutex.set(instanceKey, promise);
|
|
17956
|
+
try {
|
|
17957
|
+
return await promise;
|
|
17958
|
+
} finally {
|
|
17959
|
+
tokenMutex.delete(instanceKey);
|
|
17960
|
+
}
|
|
17961
|
+
}
|
|
17962
|
+
function invalidateAmToken(credentials) {
|
|
17963
|
+
tokenCache.delete(credentials.hostname);
|
|
17964
|
+
}
|
|
17965
|
+
async function ocapiGet(path27, credentials, ocapiVersion = DEFAULT_OCAPI_VERSION) {
|
|
17966
|
+
const doRequest = async () => {
|
|
17967
|
+
const token = await getAmToken(credentials);
|
|
17968
|
+
const baseUrl = `https://${credentials.hostname}/s/-/dw/data/${ocapiVersion}`;
|
|
17969
|
+
const url = `${baseUrl}${path27.startsWith("/") ? path27 : "/" + path27}`;
|
|
17970
|
+
const resp = await fetch(url, {
|
|
17971
|
+
method: "GET",
|
|
17972
|
+
headers: {
|
|
17973
|
+
Authorization: `Bearer ${token}`,
|
|
17974
|
+
"Content-Type": "application/json"
|
|
17975
|
+
}
|
|
17976
|
+
});
|
|
17977
|
+
const status = resp.status;
|
|
17978
|
+
let body = null;
|
|
17979
|
+
try {
|
|
17980
|
+
body = await resp.json();
|
|
17981
|
+
} catch {
|
|
17982
|
+
body = null;
|
|
17983
|
+
}
|
|
17984
|
+
return { ok: resp.ok, status, body };
|
|
17985
|
+
};
|
|
17986
|
+
const first = await doRequest();
|
|
17987
|
+
if (first.status === 401) {
|
|
17988
|
+
invalidateAmToken(credentials);
|
|
17989
|
+
return doRequest();
|
|
17990
|
+
}
|
|
17991
|
+
return first;
|
|
17992
|
+
}
|
|
17993
|
+
async function ocapiPost(path27, body, credentials, ocapiVersion = DEFAULT_OCAPI_VERSION) {
|
|
17994
|
+
const doRequest = async () => {
|
|
17995
|
+
const token = await getAmToken(credentials);
|
|
17996
|
+
const baseUrl = `https://${credentials.hostname}/s/-/dw/data/${ocapiVersion}`;
|
|
17997
|
+
const url = `${baseUrl}${path27.startsWith("/") ? path27 : "/" + path27}`;
|
|
17998
|
+
const resp = await fetch(url, {
|
|
17999
|
+
method: "POST",
|
|
18000
|
+
headers: {
|
|
18001
|
+
Authorization: `Bearer ${token}`,
|
|
18002
|
+
"Content-Type": "application/json"
|
|
18003
|
+
},
|
|
18004
|
+
body: JSON.stringify(body)
|
|
18005
|
+
});
|
|
18006
|
+
const status = resp.status;
|
|
18007
|
+
let respBody = null;
|
|
18008
|
+
try {
|
|
18009
|
+
respBody = await resp.json();
|
|
18010
|
+
} catch {
|
|
18011
|
+
respBody = null;
|
|
18012
|
+
}
|
|
18013
|
+
return { ok: resp.ok, status, body: respBody };
|
|
18014
|
+
};
|
|
18015
|
+
const first = await doRequest();
|
|
18016
|
+
if (first.status === 401) {
|
|
18017
|
+
invalidateAmToken(credentials);
|
|
18018
|
+
return doRequest();
|
|
18019
|
+
}
|
|
18020
|
+
return first;
|
|
17476
18021
|
}
|
|
17477
18022
|
|
|
18023
|
+
// src/sfcc/setup-status.ts
|
|
18024
|
+
async function sfccSetupStatusTool(deps) {
|
|
18025
|
+
const lines = ["## SFCC Setup Status\n"];
|
|
18026
|
+
const apiKeyOk = Boolean(deps.apiKey);
|
|
18027
|
+
lines.push(`1. Bridge API Key: ${apiKeyOk ? "\u2713 Resolved" : "\u2717 Missing (set BAPI_API_KEY)"}`);
|
|
18028
|
+
const repoOk = Boolean(deps.repoName);
|
|
18029
|
+
lines.push(`2. Repo Name: ${repoOk ? `\u2713 Set (${deps.repoName})` : "\u2717 Not set (set BAPI_REPO_NAME)"}`);
|
|
18030
|
+
let versionStatus = "\u2717 Not set";
|
|
18031
|
+
let resolvedVersion = null;
|
|
18032
|
+
if (apiKeyOk && repoOk) {
|
|
18033
|
+
try {
|
|
18034
|
+
resolvedVersion = await getSfccVersionConfig(
|
|
18035
|
+
deps.buildGetUrl,
|
|
18036
|
+
deps.getGetHeaders,
|
|
18037
|
+
deps.repoName
|
|
18038
|
+
);
|
|
18039
|
+
if (resolvedVersion === null) {
|
|
18040
|
+
versionStatus = "\u2717 Not set (configure the 'version' field in Bridge API project settings)";
|
|
18041
|
+
} else if (!SFCC_VERSIONS.includes(resolvedVersion)) {
|
|
18042
|
+
versionStatus = `\u2717 '${resolvedVersion}' is not an SFCC version (expected: ${SFCC_VERSIONS.join(", ")})`;
|
|
18043
|
+
} else {
|
|
18044
|
+
versionStatus = `\u2713 '${resolvedVersion}'`;
|
|
18045
|
+
}
|
|
18046
|
+
} catch {
|
|
18047
|
+
versionStatus = "\u2717 Could not read (Bridge API error)";
|
|
18048
|
+
}
|
|
18049
|
+
} else {
|
|
18050
|
+
versionStatus = "\u2014 Skipped (Bridge API not configured)";
|
|
18051
|
+
}
|
|
18052
|
+
lines.push(`3. SFCC Version: ${versionStatus}`);
|
|
18053
|
+
let credStatus = "\u2717 Missing";
|
|
18054
|
+
let resolvedCredentials = null;
|
|
18055
|
+
try {
|
|
18056
|
+
const result = await resolveSfccCredentials();
|
|
18057
|
+
if (result.ok) {
|
|
18058
|
+
resolvedCredentials = {
|
|
18059
|
+
hostname: result.credentials.hostname.split(".")[0] ?? result.credentials.hostname,
|
|
18060
|
+
source: result.credentials.source
|
|
18061
|
+
};
|
|
18062
|
+
credStatus = `\u2713 Found (${resolvedCredentials.source})`;
|
|
18063
|
+
} else {
|
|
18064
|
+
credStatus = `\u2717 ${result.error}`;
|
|
18065
|
+
}
|
|
18066
|
+
} catch (err) {
|
|
18067
|
+
credStatus = `\u2717 Resolution error: ${err instanceof Error ? err.message : String(err)}`;
|
|
18068
|
+
}
|
|
18069
|
+
lines.push(`4. dw.json / Credentials: ${credStatus}`);
|
|
18070
|
+
let tokenStatus = "\u2014 Skipped (credentials not available)";
|
|
18071
|
+
if (resolvedCredentials) {
|
|
18072
|
+
try {
|
|
18073
|
+
const credResult = await resolveSfccCredentials();
|
|
18074
|
+
if (credResult.ok) {
|
|
18075
|
+
await getAmToken(credResult.credentials);
|
|
18076
|
+
tokenStatus = `\u2713 Token acquired for instance ${resolvedCredentials.hostname}`;
|
|
18077
|
+
} else {
|
|
18078
|
+
tokenStatus = "\u2717 Credentials not resolved";
|
|
18079
|
+
}
|
|
18080
|
+
} catch (err) {
|
|
18081
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
18082
|
+
tokenStatus = `\u2717 ${msg}`;
|
|
18083
|
+
}
|
|
18084
|
+
}
|
|
18085
|
+
lines.push(`5. AM Token: ${tokenStatus}`);
|
|
18086
|
+
lines.push(
|
|
18087
|
+
"\nRun `check_permissions` to probe OCAPI access once steps 1\u20135 are all green."
|
|
18088
|
+
);
|
|
18089
|
+
return {
|
|
18090
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
18091
|
+
};
|
|
18092
|
+
}
|
|
18093
|
+
function buildSfccSetupStatusHandler(buildGetUrl2, getGetHeaders2, repoName, getApiKey) {
|
|
18094
|
+
return async (_args) => {
|
|
18095
|
+
const apiKey = await getApiKey();
|
|
18096
|
+
return sfccSetupStatusTool({ buildGetUrl: buildGetUrl2, getGetHeaders: getGetHeaders2, repoName, apiKey });
|
|
18097
|
+
};
|
|
18098
|
+
}
|
|
18099
|
+
|
|
18100
|
+
// src/sfcc/tool-wrapper.ts
|
|
18101
|
+
function notConfigured(failureClass, message) {
|
|
18102
|
+
return {
|
|
18103
|
+
content: [
|
|
18104
|
+
{
|
|
18105
|
+
type: "text",
|
|
18106
|
+
text: JSON.stringify({
|
|
18107
|
+
error: "NOT_CONFIGURED",
|
|
18108
|
+
status: 503,
|
|
18109
|
+
failure_class: failureClass,
|
|
18110
|
+
message
|
|
18111
|
+
})
|
|
18112
|
+
}
|
|
18113
|
+
]
|
|
18114
|
+
};
|
|
18115
|
+
}
|
|
18116
|
+
function withSfccGate(deps, handler) {
|
|
18117
|
+
return async (args) => {
|
|
18118
|
+
const version = await getSfccVersionConfig(
|
|
18119
|
+
deps.buildGetUrl,
|
|
18120
|
+
deps.getGetHeaders,
|
|
18121
|
+
deps.repoName
|
|
18122
|
+
);
|
|
18123
|
+
if (version === null) {
|
|
18124
|
+
return notConfigured(
|
|
18125
|
+
"bridge-auth",
|
|
18126
|
+
"Could not read the SFCC version from Bridge API (/config-field/version). Ensure your Bridge API key is set and the repo is configured. Run sfcc_setup_status for a full diagnostic."
|
|
18127
|
+
);
|
|
18128
|
+
}
|
|
18129
|
+
if (!SFCC_VERSIONS.includes(version)) {
|
|
18130
|
+
return notConfigured(
|
|
18131
|
+
"version-not-sfcc",
|
|
18132
|
+
`Repo version '${version}' is not an SFCC version. Expected one of: ${SFCC_VERSIONS.join(", ")}. Update the version field in your Bridge API project settings.`
|
|
18133
|
+
);
|
|
18134
|
+
}
|
|
18135
|
+
const explicitHostname = typeof args.instance === "string" ? args.instance : void 0;
|
|
18136
|
+
const credResult = await resolveSfccCredentials(explicitHostname);
|
|
18137
|
+
if (!credResult.ok) {
|
|
18138
|
+
return notConfigured(
|
|
18139
|
+
"missing-dw-json",
|
|
18140
|
+
`SFCC credential resolution failed: ${credResult.error} Ensure a dw.json file exists in your project root with hostname, client-id, and client-secret fields.`
|
|
18141
|
+
);
|
|
18142
|
+
}
|
|
18143
|
+
return handler(args, credResult.credentials);
|
|
18144
|
+
};
|
|
18145
|
+
}
|
|
18146
|
+
|
|
18147
|
+
// src/sfcc/ocapi-shape.ts
|
|
18148
|
+
function normalizeOcapiPage(body) {
|
|
18149
|
+
if (body === null || typeof body !== "object" || !Array.isArray(body.data) || typeof body.count !== "number") {
|
|
18150
|
+
return null;
|
|
18151
|
+
}
|
|
18152
|
+
const envelope = body;
|
|
18153
|
+
return {
|
|
18154
|
+
items: envelope.data,
|
|
18155
|
+
count: envelope.count,
|
|
18156
|
+
total: envelope.total,
|
|
18157
|
+
hasMore: envelope.next != null
|
|
18158
|
+
};
|
|
18159
|
+
}
|
|
18160
|
+
function normalizeOcapiBody(body) {
|
|
18161
|
+
if (Array.isArray(body)) {
|
|
18162
|
+
return { items: body, count: body.length, total: void 0, hasMore: false };
|
|
18163
|
+
}
|
|
18164
|
+
if (body !== null && typeof body === "object") {
|
|
18165
|
+
const obj = body;
|
|
18166
|
+
if (Array.isArray(obj.data)) {
|
|
18167
|
+
const items = obj.data;
|
|
18168
|
+
return {
|
|
18169
|
+
items,
|
|
18170
|
+
count: typeof obj.count === "number" ? obj.count : items.length,
|
|
18171
|
+
total: typeof obj.total === "number" ? obj.total : void 0,
|
|
18172
|
+
hasMore: obj.next != null
|
|
18173
|
+
};
|
|
18174
|
+
}
|
|
18175
|
+
return { items: [body], count: 1, total: void 0, hasMore: false };
|
|
18176
|
+
}
|
|
18177
|
+
return { items: [], count: 0, total: void 0, hasMore: false };
|
|
18178
|
+
}
|
|
18179
|
+
|
|
18180
|
+
// src/sfcc/permissions.ts
|
|
18181
|
+
var OCAPI_SETTINGS_READ_ONLY = (ocapiVersion) => JSON.stringify(
|
|
18182
|
+
{
|
|
18183
|
+
_v: ocapiVersion,
|
|
18184
|
+
clients: [
|
|
18185
|
+
{
|
|
18186
|
+
client_id: "<YOUR_CLIENT_ID>",
|
|
18187
|
+
resources: [
|
|
18188
|
+
{
|
|
18189
|
+
resource_id: "/system_object_definitions",
|
|
18190
|
+
methods: ["get"],
|
|
18191
|
+
read_attributes: "(**)",
|
|
18192
|
+
write_attributes: "(**)"
|
|
18193
|
+
},
|
|
18194
|
+
{
|
|
18195
|
+
resource_id: "/system_object_definitions/**",
|
|
18196
|
+
methods: ["get"],
|
|
18197
|
+
read_attributes: "(**)",
|
|
18198
|
+
write_attributes: "(**)"
|
|
18199
|
+
}
|
|
18200
|
+
]
|
|
18201
|
+
}
|
|
18202
|
+
]
|
|
18203
|
+
},
|
|
18204
|
+
null,
|
|
18205
|
+
2
|
|
18206
|
+
);
|
|
18207
|
+
var OCAPI_SETTINGS_WRITE_IMPORT = (ocapiVersion) => JSON.stringify(
|
|
18208
|
+
{
|
|
18209
|
+
_v: ocapiVersion,
|
|
18210
|
+
clients: [
|
|
18211
|
+
{
|
|
18212
|
+
client_id: "<YOUR_CLIENT_ID>",
|
|
18213
|
+
resources: [
|
|
18214
|
+
{
|
|
18215
|
+
resource_id: "/system_object_definitions",
|
|
18216
|
+
methods: ["get", "put", "patch", "delete"],
|
|
18217
|
+
read_attributes: "(**)",
|
|
18218
|
+
write_attributes: "(**)"
|
|
18219
|
+
},
|
|
18220
|
+
{
|
|
18221
|
+
resource_id: "/system_object_definitions/**",
|
|
18222
|
+
methods: ["get", "put", "patch", "delete"],
|
|
18223
|
+
read_attributes: "(**)",
|
|
18224
|
+
write_attributes: "(**)"
|
|
18225
|
+
}
|
|
18226
|
+
]
|
|
18227
|
+
}
|
|
18228
|
+
]
|
|
18229
|
+
},
|
|
18230
|
+
null,
|
|
18231
|
+
2
|
|
18232
|
+
);
|
|
18233
|
+
async function checkPermissionsTool(credentials, ocapiVersion = DEFAULT_OCAPI_VERSION) {
|
|
18234
|
+
let result;
|
|
18235
|
+
try {
|
|
18236
|
+
result = await ocapiGet("/system_object_definitions", credentials, ocapiVersion);
|
|
18237
|
+
} catch (err) {
|
|
18238
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
18239
|
+
return {
|
|
18240
|
+
content: [
|
|
18241
|
+
{
|
|
18242
|
+
type: "text",
|
|
18243
|
+
text: `OCAPI probe failed: ${msg}
|
|
18244
|
+
|
|
18245
|
+
` + ocapiSettingsInstructions(ocapiVersion)
|
|
18246
|
+
}
|
|
18247
|
+
]
|
|
18248
|
+
};
|
|
18249
|
+
}
|
|
18250
|
+
if (result.ok) {
|
|
18251
|
+
const normalized = normalizeOcapiPage(result.body);
|
|
18252
|
+
const itemCount = normalized ? normalized.count : "unknown";
|
|
18253
|
+
return {
|
|
18254
|
+
content: [
|
|
18255
|
+
{
|
|
18256
|
+
type: "text",
|
|
18257
|
+
text: `\u2713 OCAPI access confirmed.
|
|
18258
|
+
Instance: ${credentials.hostname.split(".")[0]}
|
|
18259
|
+
OCAPI version: ${ocapiVersion}
|
|
18260
|
+
GET /system_object_definitions: ${itemCount} items returned`
|
|
18261
|
+
}
|
|
18262
|
+
]
|
|
18263
|
+
};
|
|
18264
|
+
}
|
|
18265
|
+
if (result.status === 401 || result.status === 403) {
|
|
18266
|
+
return {
|
|
18267
|
+
content: [
|
|
18268
|
+
{
|
|
18269
|
+
type: "text",
|
|
18270
|
+
text: `HTTP ${result.status}: OCAPI access denied for instance ${credentials.hostname.split(".")[0]}.
|
|
18271
|
+
|
|
18272
|
+
` + ocapiSettingsInstructions(ocapiVersion)
|
|
18273
|
+
}
|
|
18274
|
+
]
|
|
18275
|
+
};
|
|
18276
|
+
}
|
|
18277
|
+
return {
|
|
18278
|
+
content: [
|
|
18279
|
+
{
|
|
18280
|
+
type: "text",
|
|
18281
|
+
text: `Unexpected OCAPI response: HTTP ${result.status}.
|
|
18282
|
+
Ensure your sandbox is running and the hostname in dw.json is correct.`
|
|
18283
|
+
}
|
|
18284
|
+
]
|
|
18285
|
+
};
|
|
18286
|
+
}
|
|
18287
|
+
function ocapiSettingsInstructions(ocapiVersion) {
|
|
18288
|
+
return `To grant OCAPI access, paste the JSON below in Business Manager:
|
|
18289
|
+
Administration > Site Development > Open Commerce API Settings \u2192 Data API tab
|
|
18290
|
+
|
|
18291
|
+
--- READ-ONLY GRANTS (v1 \u2014 required now) ---
|
|
18292
|
+
${OCAPI_SETTINGS_READ_ONLY(ocapiVersion)}
|
|
18293
|
+
|
|
18294
|
+
--- WRITE/IMPORT GRANTS (v2 \u2014 forward-looking, paste once) ---
|
|
18295
|
+
${OCAPI_SETTINGS_WRITE_IMPORT(ocapiVersion)}
|
|
18296
|
+
|
|
18297
|
+
Replace <YOUR_CLIENT_ID> with the client_id from your dw.json.`;
|
|
18298
|
+
}
|
|
18299
|
+
|
|
18300
|
+
// src/sfcc/reads-system-object.ts
|
|
18301
|
+
import path21 from "path";
|
|
18302
|
+
import { z as z3 } from "zod";
|
|
18303
|
+
|
|
18304
|
+
// src/sfcc/output.ts
|
|
18305
|
+
import path20 from "path";
|
|
18306
|
+
import { mkdir as mkdir8, writeFile as writeFile8 } from "fs/promises";
|
|
18307
|
+
var SFCC_MAX_INLINE = 5e4;
|
|
18308
|
+
function truncationNote(savedPath) {
|
|
18309
|
+
return `
|
|
18310
|
+
|
|
18311
|
+
[Response truncated \u2014 full payload saved to ${savedPath}]`;
|
|
18312
|
+
}
|
|
18313
|
+
async function truncateAndSaveIfNeeded(text, dir, filename, deps = {}) {
|
|
18314
|
+
if (text.length <= SFCC_MAX_INLINE) {
|
|
18315
|
+
return text;
|
|
18316
|
+
}
|
|
18317
|
+
const mk = deps.mkdir ?? mkdir8;
|
|
18318
|
+
const wf = deps.writeFile ?? writeFile8;
|
|
18319
|
+
const filePath = path20.join(dir, filename);
|
|
18320
|
+
try {
|
|
18321
|
+
await mk(dir, { recursive: true });
|
|
18322
|
+
await wf(filePath, text, "utf-8");
|
|
18323
|
+
} catch (err) {
|
|
18324
|
+
return text + `
|
|
18325
|
+
|
|
18326
|
+
Warning: response was NOT truncated because local save failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
18327
|
+
}
|
|
18328
|
+
return text.slice(0, SFCC_MAX_INLINE) + truncationNote(filePath);
|
|
18329
|
+
}
|
|
18330
|
+
|
|
18331
|
+
// src/sfcc/reads-system-object.ts
|
|
18332
|
+
var READ_ANNOTATIONS = {
|
|
18333
|
+
readOnlyHint: true,
|
|
18334
|
+
destructiveHint: false,
|
|
18335
|
+
idempotentHint: true,
|
|
18336
|
+
openWorldHint: true
|
|
18337
|
+
};
|
|
18338
|
+
var systemObjectListInput = z3.object({
|
|
18339
|
+
count: z3.number().optional().describe("Maximum number of system object types to return."),
|
|
18340
|
+
start: z3.number().optional().describe("Zero-based offset for paging.")
|
|
18341
|
+
});
|
|
18342
|
+
var systemObjectGetInput = z3.object({
|
|
18343
|
+
object_type: z3.string().describe('System object type identifier, e.g. "Product" or "Order".'),
|
|
18344
|
+
expand_attribute_definitions: z3.boolean().optional().describe(
|
|
18345
|
+
"When true, include the full attribute definition list in the response. May produce a large payload \u2014 auto-saved locally when oversized."
|
|
18346
|
+
)
|
|
18347
|
+
});
|
|
18348
|
+
var systemObjectAttributeSearchInput = z3.object({
|
|
18349
|
+
object_type: z3.string().describe('System object type to search within, e.g. "Order".'),
|
|
18350
|
+
query: z3.union([z3.string(), z3.record(z3.string(), z3.any())]).describe(
|
|
18351
|
+
"Search query. Pass a plain string for a text search across id and display_name, or a structured OCAPI query object (term_query, filtered_query, etc.)."
|
|
18352
|
+
),
|
|
18353
|
+
start: z3.number().optional().describe("Zero-based offset for paging."),
|
|
18354
|
+
count: z3.number().optional().describe("Maximum number of results to return."),
|
|
18355
|
+
sorts: z3.array(z3.any()).optional().describe("Array of OCAPI sort descriptors.")
|
|
18356
|
+
});
|
|
18357
|
+
function safeTimestamp() {
|
|
18358
|
+
return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
18359
|
+
}
|
|
18360
|
+
function safeType(objectType) {
|
|
18361
|
+
return encodeURIComponent(objectType).replace(/%/g, "_");
|
|
18362
|
+
}
|
|
18363
|
+
function textResult(text) {
|
|
18364
|
+
return { content: [{ type: "text", text }] };
|
|
18365
|
+
}
|
|
18366
|
+
async function saveAndReturn(text, dir, filename) {
|
|
18367
|
+
const output = await truncateAndSaveIfNeeded(text, dir, filename);
|
|
18368
|
+
return textResult(output);
|
|
18369
|
+
}
|
|
18370
|
+
function buildSystemObjectListHandler(gateDeps, getDocsDir2) {
|
|
18371
|
+
return withSfccGate(
|
|
18372
|
+
gateDeps,
|
|
18373
|
+
async (args, credentials) => {
|
|
18374
|
+
const { count, start } = systemObjectListInput.parse(args);
|
|
18375
|
+
const queryParams = {};
|
|
18376
|
+
if (count !== void 0) queryParams.count = String(count);
|
|
18377
|
+
if (start !== void 0) queryParams.start = String(start);
|
|
18378
|
+
const queryStr = Object.keys(queryParams).length > 0 ? "?" + new URLSearchParams(queryParams).toString() : "";
|
|
18379
|
+
const result = await ocapiGet(`/system_object_definitions${queryStr}`, credentials);
|
|
18380
|
+
if (!result.ok) {
|
|
18381
|
+
return textResult(
|
|
18382
|
+
JSON.stringify({ error: `OCAPI error`, status: result.status, body: result.body }, null, 2)
|
|
18383
|
+
);
|
|
18384
|
+
}
|
|
18385
|
+
const normalized = normalizeOcapiBody(result.body);
|
|
18386
|
+
const text = JSON.stringify(normalized, null, 2);
|
|
18387
|
+
const dir = path21.join(await getDocsDir2(), "sfcc");
|
|
18388
|
+
return saveAndReturn(text, dir, `system-object-list-${safeTimestamp()}.json`);
|
|
18389
|
+
}
|
|
18390
|
+
);
|
|
18391
|
+
}
|
|
18392
|
+
function buildSystemObjectGetHandler(gateDeps, getDocsDir2) {
|
|
18393
|
+
return withSfccGate(
|
|
18394
|
+
gateDeps,
|
|
18395
|
+
async (args, credentials) => {
|
|
18396
|
+
const { object_type, expand_attribute_definitions } = systemObjectGetInput.parse(args);
|
|
18397
|
+
const encodedType = encodeURIComponent(object_type);
|
|
18398
|
+
const expandParam = expand_attribute_definitions === true ? "?expand=attribute_definitions" : "";
|
|
18399
|
+
const result = await ocapiGet(
|
|
18400
|
+
`/system_object_definitions/${encodedType}${expandParam}`,
|
|
18401
|
+
credentials
|
|
18402
|
+
);
|
|
18403
|
+
if (!result.ok) {
|
|
18404
|
+
return textResult(
|
|
18405
|
+
JSON.stringify({ error: `OCAPI error`, status: result.status, body: result.body }, null, 2)
|
|
18406
|
+
);
|
|
18407
|
+
}
|
|
18408
|
+
const normalized = normalizeOcapiBody(result.body);
|
|
18409
|
+
const text = JSON.stringify(normalized, null, 2);
|
|
18410
|
+
const dir = path21.join(await getDocsDir2(), "sfcc");
|
|
18411
|
+
return saveAndReturn(
|
|
18412
|
+
text,
|
|
18413
|
+
dir,
|
|
18414
|
+
`system-object-get-${safeType(object_type)}-${safeTimestamp()}.json`
|
|
18415
|
+
);
|
|
18416
|
+
}
|
|
18417
|
+
);
|
|
18418
|
+
}
|
|
18419
|
+
function buildSystemObjectAttributeSearchHandler(gateDeps, getDocsDir2) {
|
|
18420
|
+
return withSfccGate(
|
|
18421
|
+
gateDeps,
|
|
18422
|
+
async (args, credentials) => {
|
|
18423
|
+
const { object_type, query, start, count, sorts } = systemObjectAttributeSearchInput.parse(args);
|
|
18424
|
+
const encodedType = encodeURIComponent(object_type);
|
|
18425
|
+
const resolvedQuery = typeof query === "string" ? { text_query: { fields: ["id", "display_name"], search_phrase: query } } : query;
|
|
18426
|
+
const postBody = { query: resolvedQuery };
|
|
18427
|
+
if (start !== void 0) postBody.start = start;
|
|
18428
|
+
if (count !== void 0) postBody.count = count;
|
|
18429
|
+
if (sorts !== void 0) postBody.sorts = sorts;
|
|
18430
|
+
const result = await ocapiPost(
|
|
18431
|
+
`/system_object_definitions/${encodedType}/attribute_definition_search`,
|
|
18432
|
+
postBody,
|
|
18433
|
+
credentials
|
|
18434
|
+
);
|
|
18435
|
+
if (!result.ok) {
|
|
18436
|
+
return textResult(
|
|
18437
|
+
JSON.stringify({ error: `OCAPI error`, status: result.status, body: result.body }, null, 2)
|
|
18438
|
+
);
|
|
18439
|
+
}
|
|
18440
|
+
const normalized = normalizeOcapiBody(result.body);
|
|
18441
|
+
const text = JSON.stringify(normalized, null, 2);
|
|
18442
|
+
const dir = path21.join(await getDocsDir2(), "sfcc");
|
|
18443
|
+
return saveAndReturn(
|
|
18444
|
+
text,
|
|
18445
|
+
dir,
|
|
18446
|
+
`system-object-search-${safeType(object_type)}-${safeTimestamp()}.json`
|
|
18447
|
+
);
|
|
18448
|
+
}
|
|
18449
|
+
);
|
|
18450
|
+
}
|
|
18451
|
+
function registerSystemObjectReadTools(registerTool2, deps) {
|
|
18452
|
+
const { gateDeps, getDocsDir: getDocsDir2 } = deps;
|
|
18453
|
+
registerTool2(
|
|
18454
|
+
"system_object_list",
|
|
18455
|
+
{
|
|
18456
|
+
description: "List all system object types from the developer sandbox via GET /system_object_definitions. Read-only. Accepts optional `count` and `start` for paging (OCAPI default pagination applies when omitted). Oversized outputs are auto-saved locally and previewed inline.",
|
|
18457
|
+
inputSchema: systemObjectListInput,
|
|
18458
|
+
annotations: READ_ANNOTATIONS
|
|
18459
|
+
},
|
|
18460
|
+
buildSystemObjectListHandler(gateDeps, getDocsDir2)
|
|
18461
|
+
);
|
|
18462
|
+
registerTool2(
|
|
18463
|
+
"system_object_get",
|
|
18464
|
+
{
|
|
18465
|
+
description: "Retrieve a system object type from the developer sandbox. Read-only. GET /system_object_definitions/{type}. Prefer system_object_attribute_search for targeted attribute lookups \u2014 use this for a full type dump or expanded attribute list (expand_attribute_definitions=true). Oversized payloads are auto-saved locally.",
|
|
18466
|
+
inputSchema: systemObjectGetInput,
|
|
18467
|
+
annotations: READ_ANNOTATIONS
|
|
18468
|
+
},
|
|
18469
|
+
buildSystemObjectGetHandler(gateDeps, getDocsDir2)
|
|
18470
|
+
);
|
|
18471
|
+
registerTool2(
|
|
18472
|
+
"system_object_attribute_search",
|
|
18473
|
+
{
|
|
18474
|
+
description: "Search attribute definitions for a system object type. Read-only. POST /system_object_definitions/{type}/attribute_definition_search. Prefer over system_object_get+expand for targeted c_ attribute lookups. Pass a plain string for text search or a structured OCAPI query object. Oversized results are auto-saved locally.",
|
|
18475
|
+
inputSchema: systemObjectAttributeSearchInput,
|
|
18476
|
+
annotations: READ_ANNOTATIONS
|
|
18477
|
+
},
|
|
18478
|
+
buildSystemObjectAttributeSearchHandler(gateDeps, getDocsDir2)
|
|
18479
|
+
);
|
|
18480
|
+
}
|
|
18481
|
+
|
|
18482
|
+
// src/sfcc/reads-custom-object-def.ts
|
|
18483
|
+
import path22 from "path";
|
|
18484
|
+
import { z as z4 } from "zod";
|
|
18485
|
+
var READ_ANNOTATIONS2 = {
|
|
18486
|
+
readOnlyHint: true,
|
|
18487
|
+
destructiveHint: false,
|
|
18488
|
+
idempotentHint: true,
|
|
18489
|
+
openWorldHint: true
|
|
18490
|
+
};
|
|
18491
|
+
var customObjectListInput = z4.object({
|
|
18492
|
+
count: z4.number().optional().describe("Maximum number of custom object types to return."),
|
|
18493
|
+
start: z4.number().optional().describe("Zero-based offset for paging.")
|
|
18494
|
+
});
|
|
18495
|
+
var customObjectGetInput = z4.object({
|
|
18496
|
+
object_type: z4.string().describe(
|
|
18497
|
+
'Custom object type identifier, e.g. "GiftCertificate" or a custom type id starting with "c_".'
|
|
18498
|
+
)
|
|
18499
|
+
});
|
|
18500
|
+
function safeTimestamp2() {
|
|
18501
|
+
return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
18502
|
+
}
|
|
18503
|
+
function safeType2(objectType) {
|
|
18504
|
+
return encodeURIComponent(objectType).replace(/%/g, "_");
|
|
18505
|
+
}
|
|
18506
|
+
function textResult2(text) {
|
|
18507
|
+
return { content: [{ type: "text", text }] };
|
|
18508
|
+
}
|
|
18509
|
+
async function saveAndReturn2(text, dir, filename) {
|
|
18510
|
+
const output = await truncateAndSaveIfNeeded(text, dir, filename);
|
|
18511
|
+
return textResult2(output);
|
|
18512
|
+
}
|
|
18513
|
+
function buildCustomObjectListHandler(gateDeps, getDocsDir2) {
|
|
18514
|
+
return withSfccGate(
|
|
18515
|
+
gateDeps,
|
|
18516
|
+
async (args, credentials) => {
|
|
18517
|
+
const { count, start } = customObjectListInput.parse(args);
|
|
18518
|
+
const queryParams = {};
|
|
18519
|
+
if (count !== void 0) queryParams.count = String(count);
|
|
18520
|
+
if (start !== void 0) queryParams.start = String(start);
|
|
18521
|
+
const queryStr = Object.keys(queryParams).length > 0 ? "?" + new URLSearchParams(queryParams).toString() : "";
|
|
18522
|
+
const result = await ocapiGet(`/custom_object_definitions${queryStr}`, credentials);
|
|
18523
|
+
if (!result.ok) {
|
|
18524
|
+
return textResult2(
|
|
18525
|
+
JSON.stringify({ error: "OCAPI error", status: result.status, body: result.body }, null, 2)
|
|
18526
|
+
);
|
|
18527
|
+
}
|
|
18528
|
+
const normalized = normalizeOcapiBody(result.body);
|
|
18529
|
+
const text = JSON.stringify(normalized, null, 2);
|
|
18530
|
+
const dir = path22.join(await getDocsDir2(), "sfcc");
|
|
18531
|
+
return saveAndReturn2(text, dir, `custom-object-def-list-${safeTimestamp2()}.json`);
|
|
18532
|
+
}
|
|
18533
|
+
);
|
|
18534
|
+
}
|
|
18535
|
+
function buildCustomObjectGetHandler(gateDeps, getDocsDir2) {
|
|
18536
|
+
return withSfccGate(
|
|
18537
|
+
gateDeps,
|
|
18538
|
+
async (args, credentials) => {
|
|
18539
|
+
const { object_type } = customObjectGetInput.parse(args);
|
|
18540
|
+
const encodedType = encodeURIComponent(object_type);
|
|
18541
|
+
const result = await ocapiGet(`/custom_object_definitions/${encodedType}`, credentials);
|
|
18542
|
+
if (!result.ok) {
|
|
18543
|
+
return textResult2(
|
|
18544
|
+
JSON.stringify({ error: "OCAPI error", status: result.status, body: result.body }, null, 2)
|
|
18545
|
+
);
|
|
18546
|
+
}
|
|
18547
|
+
const normalized = normalizeOcapiBody(result.body);
|
|
18548
|
+
const text = JSON.stringify(normalized, null, 2);
|
|
18549
|
+
const dir = path22.join(await getDocsDir2(), "sfcc");
|
|
18550
|
+
return saveAndReturn2(
|
|
18551
|
+
text,
|
|
18552
|
+
dir,
|
|
18553
|
+
`custom-object-def-${safeType2(object_type)}-${safeTimestamp2()}.json`
|
|
18554
|
+
);
|
|
18555
|
+
}
|
|
18556
|
+
);
|
|
18557
|
+
}
|
|
18558
|
+
function registerSfccCustomObjectDefReadTools(registerTool2, deps) {
|
|
18559
|
+
const { gateDeps, getDocsDir: getDocsDir2 } = deps;
|
|
18560
|
+
registerTool2(
|
|
18561
|
+
"custom_object_definition_list",
|
|
18562
|
+
{
|
|
18563
|
+
description: "List all custom object type definitions from the developer sandbox via GET /custom_object_definitions. Read-only introspection; type definitions cannot be created or updated via OCAPI (v2 metadata-import only). Optional `count` and `start` for paging. Oversized outputs auto-saved locally.",
|
|
18564
|
+
inputSchema: customObjectListInput,
|
|
18565
|
+
annotations: READ_ANNOTATIONS2
|
|
18566
|
+
},
|
|
18567
|
+
buildCustomObjectListHandler(gateDeps, getDocsDir2)
|
|
18568
|
+
);
|
|
18569
|
+
registerTool2(
|
|
18570
|
+
"custom_object_definition_get",
|
|
18571
|
+
{
|
|
18572
|
+
description: "Retrieve an EXISTING custom object type from the developer sandbox via GET /custom_object_definitions/{type}. Returns the type with key_definition and attribute_definitions/attribute_groups. Read-only introspection; type definitions cannot be created or updated via OCAPI (v2 metadata-import only). Oversized payloads auto-saved locally.",
|
|
18573
|
+
inputSchema: customObjectGetInput,
|
|
18574
|
+
annotations: READ_ANNOTATIONS2
|
|
18575
|
+
},
|
|
18576
|
+
buildCustomObjectGetHandler(gateDeps, getDocsDir2)
|
|
18577
|
+
);
|
|
18578
|
+
}
|
|
18579
|
+
|
|
18580
|
+
// src/sfcc/reads-site-preference.ts
|
|
18581
|
+
import path23 from "path";
|
|
18582
|
+
import { z as z5 } from "zod";
|
|
18583
|
+
var READ_ANNOTATIONS3 = {
|
|
18584
|
+
readOnlyHint: true,
|
|
18585
|
+
destructiveHint: false,
|
|
18586
|
+
idempotentHint: true,
|
|
18587
|
+
openWorldHint: true
|
|
18588
|
+
};
|
|
18589
|
+
var INSTANCE_ENUM = z5.enum(["staging", "development", "sandbox", "production"]);
|
|
18590
|
+
var INSTANCE_DESCRIBE = "OCAPI instance context. v1 supports the 'sandbox' context only; any other value is rejected with a validation error. Defaults to 'sandbox'.";
|
|
18591
|
+
var sitePreferenceGetInput = z5.object({
|
|
18592
|
+
group: z5.string().describe("Preference group ID, e.g. 'Account' or 'General'."),
|
|
18593
|
+
instance: INSTANCE_ENUM.optional().default("sandbox").describe(INSTANCE_DESCRIBE),
|
|
18594
|
+
start: z5.number().optional().describe("Zero-based offset for paging."),
|
|
18595
|
+
count: z5.number().optional().describe("Maximum number of preferences to return.")
|
|
18596
|
+
});
|
|
18597
|
+
var sitePreferenceSearchInput = z5.object({
|
|
18598
|
+
group: z5.string().describe("Preference group ID to search within."),
|
|
18599
|
+
instance: INSTANCE_ENUM.optional().default("sandbox").describe(INSTANCE_DESCRIBE),
|
|
18600
|
+
query: z5.union([z5.string(), z5.record(z5.string(), z5.any())]).describe(
|
|
18601
|
+
"Search query. Pass a plain string for text search across preference ids and values, or a structured OCAPI query object (term_query, filtered_query, etc.)."
|
|
18602
|
+
),
|
|
18603
|
+
start: z5.number().optional().describe("Zero-based offset for paging."),
|
|
18604
|
+
count: z5.number().optional().describe("Maximum number of results to return."),
|
|
18605
|
+
sorts: z5.array(z5.any()).optional().describe("Array of OCAPI sort descriptors.")
|
|
18606
|
+
});
|
|
18607
|
+
function safeTimestamp3() {
|
|
18608
|
+
return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
18609
|
+
}
|
|
18610
|
+
function safeGroup(group) {
|
|
18611
|
+
return encodeURIComponent(group).replace(/%/g, "_");
|
|
18612
|
+
}
|
|
18613
|
+
function textResult3(text) {
|
|
18614
|
+
return { content: [{ type: "text", text }] };
|
|
18615
|
+
}
|
|
18616
|
+
async function saveAndReturn3(text, dir, filename) {
|
|
18617
|
+
const output = await truncateAndSaveIfNeeded(text, dir, filename);
|
|
18618
|
+
return textResult3(output);
|
|
18619
|
+
}
|
|
18620
|
+
function rejectIfNotSandbox(instance) {
|
|
18621
|
+
if (instance !== "sandbox") {
|
|
18622
|
+
return textResult3(
|
|
18623
|
+
JSON.stringify({
|
|
18624
|
+
error: "VALIDATION_ERROR",
|
|
18625
|
+
status: 400,
|
|
18626
|
+
message: `v1 only supports the 'sandbox' instance context. Received: '${instance}'.`
|
|
18627
|
+
}, null, 2)
|
|
18628
|
+
);
|
|
18629
|
+
}
|
|
18630
|
+
return null;
|
|
18631
|
+
}
|
|
18632
|
+
function buildSitePreferenceGetHandler(gateDeps, getDocsDir2) {
|
|
18633
|
+
return withSfccGate(
|
|
18634
|
+
gateDeps,
|
|
18635
|
+
async (args, credentials) => {
|
|
18636
|
+
const { group, instance, start, count } = sitePreferenceGetInput.parse(args);
|
|
18637
|
+
const guard = rejectIfNotSandbox(instance);
|
|
18638
|
+
if (guard) return guard;
|
|
18639
|
+
const encodedGroup = encodeURIComponent(group);
|
|
18640
|
+
const queryParams = {};
|
|
18641
|
+
if (start !== void 0) queryParams.start = String(start);
|
|
18642
|
+
if (count !== void 0) queryParams.count = String(count);
|
|
18643
|
+
const queryStr = Object.keys(queryParams).length > 0 ? "?" + new URLSearchParams(queryParams).toString() : "";
|
|
18644
|
+
const result = await ocapiGet(
|
|
18645
|
+
`/site_preferences/preference_groups/${encodedGroup}/${instance}${queryStr}`,
|
|
18646
|
+
credentials
|
|
18647
|
+
);
|
|
18648
|
+
if (!result.ok) {
|
|
18649
|
+
return textResult3(
|
|
18650
|
+
JSON.stringify({ error: "OCAPI error", status: result.status, body: result.body }, null, 2)
|
|
18651
|
+
);
|
|
18652
|
+
}
|
|
18653
|
+
const normalized = normalizeOcapiBody(result.body);
|
|
18654
|
+
const text = JSON.stringify(normalized, null, 2);
|
|
18655
|
+
const dir = path23.join(await getDocsDir2(), "sfcc");
|
|
18656
|
+
return saveAndReturn3(
|
|
18657
|
+
text,
|
|
18658
|
+
dir,
|
|
18659
|
+
`site-preference-get-${safeGroup(group)}-${safeTimestamp3()}.json`
|
|
18660
|
+
);
|
|
18661
|
+
}
|
|
18662
|
+
);
|
|
18663
|
+
}
|
|
18664
|
+
function buildSitePreferenceSearchHandler(gateDeps, getDocsDir2) {
|
|
18665
|
+
return withSfccGate(
|
|
18666
|
+
gateDeps,
|
|
18667
|
+
async (args, credentials) => {
|
|
18668
|
+
const { group, instance, query, start, count, sorts } = sitePreferenceSearchInput.parse(args);
|
|
18669
|
+
const guard = rejectIfNotSandbox(instance);
|
|
18670
|
+
if (guard) return guard;
|
|
18671
|
+
const encodedGroup = encodeURIComponent(group);
|
|
18672
|
+
const resolvedQuery = typeof query === "string" ? { text_query: { fields: ["id", "value"], search_phrase: query } } : query;
|
|
18673
|
+
const postBody = { query: resolvedQuery };
|
|
18674
|
+
if (start !== void 0) postBody.start = start;
|
|
18675
|
+
if (count !== void 0) postBody.count = count;
|
|
18676
|
+
if (sorts !== void 0) postBody.sorts = sorts;
|
|
18677
|
+
const result = await ocapiPost(
|
|
18678
|
+
`/site_preferences/preference_groups/${encodedGroup}/${instance}/preference_search`,
|
|
18679
|
+
postBody,
|
|
18680
|
+
credentials
|
|
18681
|
+
);
|
|
18682
|
+
if (!result.ok) {
|
|
18683
|
+
return textResult3(
|
|
18684
|
+
JSON.stringify({ error: "OCAPI error", status: result.status, body: result.body }, null, 2)
|
|
18685
|
+
);
|
|
18686
|
+
}
|
|
18687
|
+
const normalized = normalizeOcapiBody(result.body);
|
|
18688
|
+
const text = JSON.stringify(normalized, null, 2);
|
|
18689
|
+
const dir = path23.join(await getDocsDir2(), "sfcc");
|
|
18690
|
+
return saveAndReturn3(
|
|
18691
|
+
text,
|
|
18692
|
+
dir,
|
|
18693
|
+
`site-preference-search-${safeGroup(group)}-${safeTimestamp3()}.json`
|
|
18694
|
+
);
|
|
18695
|
+
}
|
|
18696
|
+
);
|
|
18697
|
+
}
|
|
18698
|
+
function registerSitePreferenceTools(registerTool2, deps) {
|
|
18699
|
+
const { gateDeps, getDocsDir: getDocsDir2 } = deps;
|
|
18700
|
+
registerTool2(
|
|
18701
|
+
"site_preference_get",
|
|
18702
|
+
{
|
|
18703
|
+
description: "Read effective preferences for a site preference group from the developer sandbox via GET /site_preferences/preference_groups/{group}/sandbox. Read-only; v1 supports sandboxes only (non-sandbox instances are rejected). Accepts optional `start` and `count` for paging. Oversized payloads are auto-saved locally and previewed inline.",
|
|
18704
|
+
inputSchema: sitePreferenceGetInput,
|
|
18705
|
+
annotations: READ_ANNOTATIONS3
|
|
18706
|
+
},
|
|
18707
|
+
buildSitePreferenceGetHandler(gateDeps, getDocsDir2)
|
|
18708
|
+
);
|
|
18709
|
+
registerTool2(
|
|
18710
|
+
"site_preference_search",
|
|
18711
|
+
{
|
|
18712
|
+
description: "Search/filter preferences within a site preference group from the developer sandbox via POST /site_preferences/preference_groups/{group}/sandbox/preference_search. Read-only; v1 sandbox only. Pass a plain string for text search or a structured OCAPI query object. Oversized results are auto-saved locally.",
|
|
18713
|
+
inputSchema: sitePreferenceSearchInput,
|
|
18714
|
+
annotations: READ_ANNOTATIONS3
|
|
18715
|
+
},
|
|
18716
|
+
buildSitePreferenceSearchHandler(gateDeps, getDocsDir2)
|
|
18717
|
+
);
|
|
18718
|
+
}
|
|
18719
|
+
|
|
18720
|
+
// src/sfcc/register.ts
|
|
18721
|
+
function registerSfccTools(registerTool2, deps) {
|
|
18722
|
+
const gateDeps = {
|
|
18723
|
+
buildGetUrl: deps.buildGetUrl,
|
|
18724
|
+
getGetHeaders: deps.getGetHeaders,
|
|
18725
|
+
repoName: deps.repoName
|
|
18726
|
+
};
|
|
18727
|
+
registerTool2(
|
|
18728
|
+
"sfcc_setup_status",
|
|
18729
|
+
{
|
|
18730
|
+
description: "Report on every SFCC prerequisite: Bridge API key, repo name, version config, dw.json presence/uniqueness, and AM token acquisition. Always-registered; returns status without requiring full SFCC configuration to be complete.",
|
|
18731
|
+
inputSchema: z6.object({}),
|
|
18732
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
|
|
18733
|
+
},
|
|
18734
|
+
buildSfccSetupStatusHandler(
|
|
18735
|
+
deps.buildGetUrl,
|
|
18736
|
+
deps.getGetHeaders,
|
|
18737
|
+
deps.repoName,
|
|
18738
|
+
deps.getResolvedApiKey
|
|
18739
|
+
)
|
|
18740
|
+
);
|
|
18741
|
+
const gatedCheckPermissions = withSfccGate(
|
|
18742
|
+
gateDeps,
|
|
18743
|
+
async (_args, credentials) => checkPermissionsTool(credentials)
|
|
18744
|
+
);
|
|
18745
|
+
registerTool2(
|
|
18746
|
+
"check_permissions",
|
|
18747
|
+
{
|
|
18748
|
+
description: "Probe SFCC OCAPI access via GET /system_object_definitions. On 200: reports OK and the detected OCAPI version. On 401/403: prints the exact OCAPI Settings JSON to paste in Business Manager (split read-only vs. write/import grants).",
|
|
18749
|
+
inputSchema: z6.object({
|
|
18750
|
+
instance: z6.string().optional().describe("Explicit sandbox hostname to use instead of dw.json auto-detection.")
|
|
18751
|
+
}),
|
|
18752
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
|
|
18753
|
+
},
|
|
18754
|
+
gatedCheckPermissions
|
|
18755
|
+
);
|
|
18756
|
+
if (deps.includeReadTools) {
|
|
18757
|
+
registerSystemObjectReadTools(registerTool2, {
|
|
18758
|
+
gateDeps,
|
|
18759
|
+
getDocsDir: deps.getDocsDir
|
|
18760
|
+
});
|
|
18761
|
+
registerSfccCustomObjectDefReadTools(registerTool2, {
|
|
18762
|
+
gateDeps,
|
|
18763
|
+
getDocsDir: deps.getDocsDir
|
|
18764
|
+
});
|
|
18765
|
+
registerSitePreferenceTools(registerTool2, {
|
|
18766
|
+
gateDeps,
|
|
18767
|
+
getDocsDir: deps.getDocsDir
|
|
18768
|
+
});
|
|
18769
|
+
}
|
|
18770
|
+
}
|
|
18771
|
+
|
|
18772
|
+
// src/index.ts
|
|
18773
|
+
init_mcp_profile();
|
|
18774
|
+
|
|
17478
18775
|
// src/conductor/cli.ts
|
|
17479
18776
|
init_errors();
|
|
17480
18777
|
init_store();
|
|
@@ -17648,6 +18945,7 @@ function inspectConductorGitHooks(deps = {}) {
|
|
|
17648
18945
|
|
|
17649
18946
|
// src/conductor/git-producer.ts
|
|
17650
18947
|
init_git_ci_types();
|
|
18948
|
+
init_producer_ledger();
|
|
17651
18949
|
var COMMITTED_REF_PHASE = "committed";
|
|
17652
18950
|
function buildCommitCreatedEventInput(context, metadata) {
|
|
17653
18951
|
const details = {
|
|
@@ -17749,6 +19047,7 @@ async function runReferenceTransactionHookProducer(args, deps = {}) {
|
|
|
17749
19047
|
|
|
17750
19048
|
// src/conductor/doctor.ts
|
|
17751
19049
|
init_store();
|
|
19050
|
+
import { spawnSync } from "node:child_process";
|
|
17752
19051
|
async function inspectEpicTickSchedule(deps, orchestrateListOverride) {
|
|
17753
19052
|
try {
|
|
17754
19053
|
const schedRun = orchestrateListOverride ? null : await Promise.resolve().then(() => (init_schedule_run(), schedule_run_exports));
|
|
@@ -17815,6 +19114,46 @@ function inspectMcpProfile(env, epicTick) {
|
|
|
17815
19114
|
}
|
|
17816
19115
|
return { resolved_profile, conductor_context_detected, degraded, warnings };
|
|
17817
19116
|
}
|
|
19117
|
+
function inspectLocalMerge(runCommand) {
|
|
19118
|
+
const run = runCommand ?? ((cmd, args) => {
|
|
19119
|
+
try {
|
|
19120
|
+
const r = spawnSync(cmd, args, {
|
|
19121
|
+
encoding: "utf8",
|
|
19122
|
+
timeout: 1e4,
|
|
19123
|
+
env: { ...process.env, GH_PROMPT_DISABLED: "1" }
|
|
19124
|
+
});
|
|
19125
|
+
return { status: r.status };
|
|
19126
|
+
} catch {
|
|
19127
|
+
return { status: null };
|
|
19128
|
+
}
|
|
19129
|
+
});
|
|
19130
|
+
let gh_available = false;
|
|
19131
|
+
let gh_authed = false;
|
|
19132
|
+
try {
|
|
19133
|
+
gh_available = run("gh", ["--version"]).status === 0;
|
|
19134
|
+
} catch {
|
|
19135
|
+
gh_available = false;
|
|
19136
|
+
}
|
|
19137
|
+
if (gh_available) {
|
|
19138
|
+
try {
|
|
19139
|
+
gh_authed = run("gh", ["auth", "status"]).status === 0;
|
|
19140
|
+
} catch {
|
|
19141
|
+
gh_authed = false;
|
|
19142
|
+
}
|
|
19143
|
+
}
|
|
19144
|
+
const warnings = [];
|
|
19145
|
+
if (!gh_available) {
|
|
19146
|
+
warnings.push(
|
|
19147
|
+
"`gh` is not installed or not on PATH. Local merge (policy_json.local_merge.enabled) cannot run; install the GitHub CLI to enable conductor-driven merges."
|
|
19148
|
+
);
|
|
19149
|
+
} else if (!gh_authed) {
|
|
19150
|
+
warnings.push(
|
|
19151
|
+
"`gh` is installed but not authenticated (`gh auth status` failed). Run `gh auth login` to grant the conductor merge permission; otherwise local merge will emit merge.failed/skip."
|
|
19152
|
+
);
|
|
19153
|
+
}
|
|
19154
|
+
const degraded = !gh_available || !gh_authed;
|
|
19155
|
+
return { gh_available, gh_authed, degraded, warnings };
|
|
19156
|
+
}
|
|
17818
19157
|
async function buildConductorDoctorReport(deps = {}) {
|
|
17819
19158
|
const doctorLedger = deps.doctorLedger ?? doctorConductorLedger;
|
|
17820
19159
|
const inspectHooks = deps.inspectHooks ?? inspectConductorGitHooks;
|
|
@@ -17824,11 +19163,12 @@ async function buildConductorDoctorReport(deps = {}) {
|
|
|
17824
19163
|
ledger: await doctorLedger(),
|
|
17825
19164
|
git_hooks: inspectHooks(deps.hooksDeps),
|
|
17826
19165
|
epic_tick: epicTick,
|
|
17827
|
-
mcp_profile
|
|
19166
|
+
mcp_profile,
|
|
19167
|
+
local_merge: inspectLocalMerge(deps.runCommand)
|
|
17828
19168
|
};
|
|
17829
19169
|
}
|
|
17830
19170
|
function formatConductorDoctorReport(report) {
|
|
17831
|
-
const { ledger, git_hooks, epic_tick, mcp_profile } = report;
|
|
19171
|
+
const { ledger, git_hooks, epic_tick, mcp_profile, local_merge } = report;
|
|
17832
19172
|
const lines = [
|
|
17833
19173
|
"Conductor ledger doctor",
|
|
17834
19174
|
"\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
|
|
@@ -17891,6 +19231,17 @@ function formatConductorDoctorReport(report) {
|
|
|
17891
19231
|
lines.push("mcp profile warnings:");
|
|
17892
19232
|
for (const w of mcp_profile.warnings) lines.push(` - ${w}`);
|
|
17893
19233
|
}
|
|
19234
|
+
lines.push("");
|
|
19235
|
+
lines.push("Local merge capability (optional, opt-in)");
|
|
19236
|
+
lines.push("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
19237
|
+
const ghTag = local_merge.gh_available ? local_merge.gh_authed ? "[OK]" : "[WARNING] not authenticated" : "[WARNING] not installed";
|
|
19238
|
+
lines.push(`gh available: ${local_merge.gh_available} ${ghTag}`);
|
|
19239
|
+
lines.push(`gh authenticated: ${local_merge.gh_authed}`);
|
|
19240
|
+
lines.push(`degraded: ${local_merge.degraded}`);
|
|
19241
|
+
if (local_merge.warnings.length > 0) {
|
|
19242
|
+
lines.push("local merge warnings:");
|
|
19243
|
+
for (const w of local_merge.warnings) lines.push(` - ${w}`);
|
|
19244
|
+
}
|
|
17894
19245
|
return lines.join("\n");
|
|
17895
19246
|
}
|
|
17896
19247
|
|
|
@@ -18746,135 +20097,135 @@ async function runConductorCli(argv) {
|
|
|
18746
20097
|
import { generateDecisionPageHtml } from "./decision-page-template.js";
|
|
18747
20098
|
|
|
18748
20099
|
// src/decision-page-schema.ts
|
|
18749
|
-
import { z as
|
|
18750
|
-
var ActionableItemSchema =
|
|
18751
|
-
id:
|
|
20100
|
+
import { z as z7 } from "zod";
|
|
20101
|
+
var ActionableItemSchema = z7.object({
|
|
20102
|
+
id: z7.string().min(1).regex(
|
|
18752
20103
|
/^[A-Za-z0-9_-]+$/,
|
|
18753
20104
|
"id must contain only letters, digits, hyphens, or underscores"
|
|
18754
20105
|
),
|
|
18755
|
-
question:
|
|
18756
|
-
original_question:
|
|
20106
|
+
question: z7.string().min(1),
|
|
20107
|
+
original_question: z7.string().optional().describe(
|
|
18757
20108
|
"Optional display-only field: the clarifying question or critique point as originally raised; soft cap ~30 words. Omit it (or pass an empty string) for non-review callers \u2014 the renderer omits the section when it is absent or blank."
|
|
18758
20109
|
),
|
|
18759
|
-
why_it_matters:
|
|
18760
|
-
recommendation_explanation:
|
|
18761
|
-
codebase_evidence:
|
|
20110
|
+
why_it_matters: z7.string().min(1).describe("Concrete one-sentence impact of this decision; soft cap ~40 words."),
|
|
20111
|
+
recommendation_explanation: z7.string().min(1).describe("Why the recommended branch is the best choice; soft cap ~60 words."),
|
|
20112
|
+
codebase_evidence: z7.string().optional().describe(
|
|
18762
20113
|
"Optional display-only field: combined Assessment paragraph and Codebase Evidence bullet list. Rendered as escaped plain text inside a closed-by-default <details> block, which is omitted when this field is absent or blank."
|
|
18763
20114
|
),
|
|
18764
|
-
source:
|
|
20115
|
+
source: z7.string().optional().describe(
|
|
18765
20116
|
`Optional source reference from the combined review-and-resolution doc, e.g. 'Clarifying Q3 (prior round, weak concurrence)'. When absent the rendered card emits data-source="".`
|
|
18766
20117
|
),
|
|
18767
|
-
recommendation_index:
|
|
18768
|
-
options:
|
|
18769
|
-
option_consequences:
|
|
20118
|
+
recommendation_index: z7.number().int().min(0).describe("0-based index of the recommended option in the options array"),
|
|
20119
|
+
options: z7.array(z7.string().min(1)).min(2).max(4).describe("Option labels from the decision tree branches. Values are auto-generated. Must have 2\u20134 entries."),
|
|
20120
|
+
option_consequences: z7.array(z7.string().min(1)).min(2).max(4).describe(
|
|
18770
20121
|
"Behavioral consequence per branch, parallel to options. Must have 2\u20134 entries; length must equal options.length."
|
|
18771
20122
|
)
|
|
18772
20123
|
}).superRefine((item, ctx) => {
|
|
18773
20124
|
if (item.option_consequences.length !== item.options.length) {
|
|
18774
20125
|
ctx.addIssue({
|
|
18775
|
-
code:
|
|
20126
|
+
code: z7.ZodIssueCode.custom,
|
|
18776
20127
|
path: ["option_consequences"],
|
|
18777
20128
|
message: `option_consequences length (${item.option_consequences.length}) must match options length (${item.options.length}).`
|
|
18778
20129
|
});
|
|
18779
20130
|
}
|
|
18780
20131
|
if (item.recommendation_index >= item.options.length) {
|
|
18781
20132
|
ctx.addIssue({
|
|
18782
|
-
code:
|
|
20133
|
+
code: z7.ZodIssueCode.custom,
|
|
18783
20134
|
path: ["recommendation_index"],
|
|
18784
20135
|
message: `recommendation_index (${item.recommendation_index}) is out of bounds (${item.options.length} options).`
|
|
18785
20136
|
});
|
|
18786
20137
|
}
|
|
18787
20138
|
});
|
|
18788
|
-
var DecisionPageLabelsSchema =
|
|
18789
|
-
title:
|
|
18790
|
-
intro:
|
|
18791
|
-
section_heading:
|
|
18792
|
-
improvements_heading:
|
|
20139
|
+
var DecisionPageLabelsSchema = z7.object({
|
|
20140
|
+
title: z7.string().optional().describe('Overrides the page <title>/<h1> lead text (default "Review Decisions").'),
|
|
20141
|
+
intro: z7.string().optional().describe("Overrides the actionable-page intro copy shown when there are decisions."),
|
|
20142
|
+
section_heading: z7.string().optional().describe('Overrides the decision cards <h2> (default "Review Decisions").'),
|
|
20143
|
+
improvements_heading: z7.string().optional().describe('Overrides the confirmed-improvements <h2> (default "Confirmed Improvements").')
|
|
18793
20144
|
});
|
|
18794
|
-
var SystemGoalNfrSchema =
|
|
18795
|
-
category:
|
|
20145
|
+
var SystemGoalNfrSchema = z7.object({
|
|
20146
|
+
category: z7.string().min(1).describe(
|
|
18796
20147
|
"Canonical NFR category, e.g. security/privacy, performance/latency, reliability/failure-modes, observability/auditability, accessibility/UX, data-integrity/migration, compatibility, operability/config, compliance/SOC2, rollout/reversibility."
|
|
18797
20148
|
),
|
|
18798
|
-
requirement:
|
|
18799
|
-
implication:
|
|
20149
|
+
requirement: z7.string().min(1).describe("The non-functional requirement itself."),
|
|
20150
|
+
implication: z7.string().min(1).describe(
|
|
18800
20151
|
"What this requirement changes about the implementation. Required \u2014 drop the NFR rather than emit boilerplate without an implication."
|
|
18801
20152
|
),
|
|
18802
|
-
status:
|
|
20153
|
+
status: z7.enum(["confirmed", "assumed", "open"]).describe(
|
|
18803
20154
|
"confirmed = explicitly stated or observable in code; assumed = low-risk and reversible default; open = unresolved (also surface as an actionable_items card)."
|
|
18804
20155
|
)
|
|
18805
20156
|
});
|
|
18806
|
-
var SystemGoalsSchema =
|
|
18807
|
-
business_goal:
|
|
18808
|
-
desired_end_state:
|
|
18809
|
-
system_behavior:
|
|
18810
|
-
nfrs:
|
|
20157
|
+
var SystemGoalsSchema = z7.object({
|
|
20158
|
+
business_goal: z7.string().min(1).describe("The business goal this work serves."),
|
|
20159
|
+
desired_end_state: z7.string().min(1).describe("The end-state the system should reach."),
|
|
20160
|
+
system_behavior: z7.string().min(1).describe("How the system must behave / complete its task (quality attributes in prose)."),
|
|
20161
|
+
nfrs: z7.array(SystemGoalNfrSchema).optional().default([])
|
|
18811
20162
|
});
|
|
18812
|
-
var ImplementationOrderItemSchema =
|
|
18813
|
-
title:
|
|
18814
|
-
depends_on:
|
|
18815
|
-
recommended_after:
|
|
18816
|
-
rationale:
|
|
20163
|
+
var ImplementationOrderItemSchema = z7.object({
|
|
20164
|
+
title: z7.string().min(1).describe("Short title of the slice / child ticket."),
|
|
20165
|
+
depends_on: z7.array(z7.string().min(1)).optional().default([]).describe("Hard prerequisites (titles or keys) that must land first."),
|
|
20166
|
+
recommended_after: z7.array(z7.string().min(1)).optional().default([]).describe("Soft sequencing preferences \u2014 not hard blockers."),
|
|
20167
|
+
rationale: z7.string().min(1).describe("Why this slice sits at this point in the order.")
|
|
18817
20168
|
});
|
|
18818
20169
|
var DecisionPageInputShape = {
|
|
18819
|
-
ticket_key:
|
|
18820
|
-
artifact_type:
|
|
20170
|
+
ticket_key: z7.string().describe("Jira ticket key, e.g. BAPI-123"),
|
|
20171
|
+
artifact_type: z7.enum(["review_decisions", "pre_ticket_planning"]).optional().default("review_decisions").describe(
|
|
18821
20172
|
'Which flavor of page to render. "review_decisions" (default) is the ticket-review decision-capture page and is unaffected by the planning fields. "pre_ticket_planning" additionally renders the read-only system_goals and implementation_order sections for pre-ticket epic/task framing.'
|
|
18822
20173
|
),
|
|
18823
20174
|
system_goals: SystemGoalsSchema.optional().describe(
|
|
18824
20175
|
"pre_ticket_planning only: read-only business goal, desired end-state, system behavior, and classified NFRs. Unresolved (open) NFRs should ALSO be passed as actionable_items so the human can decide them."
|
|
18825
20176
|
),
|
|
18826
|
-
implementation_order:
|
|
20177
|
+
implementation_order: z7.array(ImplementationOrderItemSchema).optional().describe(
|
|
18827
20178
|
"pre_ticket_planning epic surfaces only: read-only recommended implementation order (hard depends_on vs soft recommended_after). No Jira links are created from this."
|
|
18828
20179
|
),
|
|
18829
|
-
output_subdir:
|
|
20180
|
+
output_subdir: z7.string().optional().default("review").describe(
|
|
18830
20181
|
'Optional docs-relative subdirectory to write the page under (default "review"). Validated strictly: no absolute paths, backslashes, ".." segments, null bytes, or encoded path tokens.'
|
|
18831
20182
|
),
|
|
18832
|
-
output_filename:
|
|
20183
|
+
output_filename: z7.string().optional().describe(
|
|
18833
20184
|
'Optional output filename (default "${ticket_key}-decisions.html"). Must end with .html and contain no path separators; the .html suffix is required and never auto-appended.'
|
|
18834
20185
|
),
|
|
18835
20186
|
labels: DecisionPageLabelsSchema.optional().describe(
|
|
18836
20187
|
"Optional presentation-label overrides (title, intro, section_heading, improvements_heading). Presentation-only; does not change data-testid hooks or the submitted JSON shape."
|
|
18837
20188
|
),
|
|
18838
|
-
actionable_items:
|
|
20189
|
+
actionable_items: z7.array(ActionableItemSchema).optional().default([]).describe(
|
|
18839
20190
|
"Actionable review decisions sourced from the combined review-and-resolution document. 'None of these' is auto-appended by the renderer and must not appear in options."
|
|
18840
20191
|
),
|
|
18841
|
-
clear_improvements:
|
|
18842
|
-
|
|
18843
|
-
id:
|
|
20192
|
+
clear_improvements: z7.array(
|
|
20193
|
+
z7.object({
|
|
20194
|
+
id: z7.string().min(1).describe(
|
|
18844
20195
|
"Stable identifier for the improvement. Stored for the rewrite/capture step but intentionally not rendered to the user."
|
|
18845
20196
|
),
|
|
18846
|
-
title:
|
|
18847
|
-
action:
|
|
18848
|
-
confidence:
|
|
18849
|
-
source:
|
|
20197
|
+
title: z7.string().min(1),
|
|
20198
|
+
action: z7.string().min(1),
|
|
20199
|
+
confidence: z7.string().min(1),
|
|
20200
|
+
source: z7.string().min(1).describe(
|
|
18850
20201
|
"Source reference from the evaluation. Stored for the rewrite/capture step but intentionally not rendered to the user \u2014 the confirmed-improvements list shows title/confidence/action only."
|
|
18851
20202
|
)
|
|
18852
20203
|
})
|
|
18853
20204
|
).optional().default([]).describe("Confirmed improvements displayed as informational list, not submitted.")
|
|
18854
20205
|
};
|
|
18855
|
-
var DecisionPageInputSchema =
|
|
20206
|
+
var DecisionPageInputSchema = z7.object(DecisionPageInputShape);
|
|
18856
20207
|
var DecisionPageLeanInputShape = {
|
|
18857
|
-
ticket_key:
|
|
18858
|
-
artifact_type:
|
|
20208
|
+
ticket_key: z7.string().describe("Jira ticket key, e.g. BAPI-123"),
|
|
20209
|
+
artifact_type: z7.enum(["review_decisions", "pre_ticket_planning"]).optional().default("review_decisions").describe(
|
|
18859
20210
|
'Which flavor of page to render. "review_decisions" (default) or "pre_ticket_planning" (adds system_goals and implementation_order sections).'
|
|
18860
20211
|
),
|
|
18861
|
-
output_subdir:
|
|
20212
|
+
output_subdir: z7.string().optional().default("review").describe(
|
|
18862
20213
|
'Optional docs-relative subdirectory to write the page under (default "review"). No absolute paths, backslashes, ".." segments, null bytes, or encoded path tokens.'
|
|
18863
20214
|
),
|
|
18864
|
-
output_filename:
|
|
20215
|
+
output_filename: z7.string().optional().describe(
|
|
18865
20216
|
'Optional output filename (default "${ticket_key}-decisions.html"). Must end with .html; no path separators.'
|
|
18866
20217
|
),
|
|
18867
20218
|
labels: DecisionPageLabelsSchema.optional().describe(
|
|
18868
20219
|
"Optional presentation-label overrides (title, intro, section_heading, improvements_heading)."
|
|
18869
20220
|
),
|
|
18870
|
-
content:
|
|
20221
|
+
content: z7.record(z7.string(), z7.unknown()).optional().describe(
|
|
18871
20222
|
"Contains deferred heavy payloads like actionable_items or system_goals."
|
|
18872
20223
|
)
|
|
18873
20224
|
};
|
|
18874
20225
|
|
|
18875
20226
|
// src/brainstorm-files.ts
|
|
18876
|
-
import { writeFile as
|
|
18877
|
-
import
|
|
20227
|
+
import { writeFile as writeFile9, mkdir as mkdir9 } from "fs/promises";
|
|
20228
|
+
import path25 from "path";
|
|
18878
20229
|
function slugify(text, maxLength = 60) {
|
|
18879
20230
|
return text.toLowerCase().replace(/[^a-z0-9\s-]/g, "").trim().replace(/\s+/g, "-").replace(/-+/g, "-").slice(0, maxLength).replace(/-$/, "");
|
|
18880
20231
|
}
|
|
@@ -18899,10 +20250,10 @@ async function saveBrainstormResultsToDir(envelope, dir, subject) {
|
|
|
18899
20250
|
continue;
|
|
18900
20251
|
}
|
|
18901
20252
|
const filename = buildBrainstormResultFilename(envelope, row, subject);
|
|
18902
|
-
const filePath =
|
|
20253
|
+
const filePath = path25.join(dir, filename);
|
|
18903
20254
|
try {
|
|
18904
|
-
await
|
|
18905
|
-
await
|
|
20255
|
+
await mkdir9(dir, { recursive: true });
|
|
20256
|
+
await writeFile9(filePath, markdown, "utf-8");
|
|
18906
20257
|
savedPaths.push(filePath);
|
|
18907
20258
|
} catch {
|
|
18908
20259
|
}
|
|
@@ -20662,7 +22013,7 @@ function parseDefaultOnEnvFlag(value) {
|
|
|
20662
22013
|
var UPGRADE_ADVICE_SURFACING_ENABLED = parseDefaultOnEnvFlag(
|
|
20663
22014
|
process.env.BAPI_MCP_UPGRADE_ADVICE_ENABLED
|
|
20664
22015
|
);
|
|
20665
|
-
var
|
|
22016
|
+
var ACTIVE_GROUPS = resolveProfiles(process.env.BRIDGE_MCP_PROFILE);
|
|
20666
22017
|
var resolvedApiKeyPromise;
|
|
20667
22018
|
async function getResolvedApiKey() {
|
|
20668
22019
|
if (!resolvedApiKeyPromise) {
|
|
@@ -20672,7 +22023,7 @@ async function getResolvedApiKey() {
|
|
|
20672
22023
|
env: process.env,
|
|
20673
22024
|
homedir: os10.homedir,
|
|
20674
22025
|
platform: process.platform,
|
|
20675
|
-
readFile: (p) =>
|
|
22026
|
+
readFile: (p) => readFile11(p, "utf-8"),
|
|
20676
22027
|
stat: (p) => stat8(p)
|
|
20677
22028
|
});
|
|
20678
22029
|
return result.ok ? result.credentials.apiKey : "";
|
|
@@ -20689,7 +22040,7 @@ async function getResolvedApiKeyForRepo(repoName) {
|
|
|
20689
22040
|
env: process.env,
|
|
20690
22041
|
homedir: os10.homedir,
|
|
20691
22042
|
platform: process.platform,
|
|
20692
|
-
readFile: (p) =>
|
|
22043
|
+
readFile: (p) => readFile11(p, "utf-8"),
|
|
20693
22044
|
stat: (p) => stat8(p)
|
|
20694
22045
|
});
|
|
20695
22046
|
return result.ok ? result.credentials.apiKey : "";
|
|
@@ -20702,9 +22053,9 @@ function buildCredentialStoreWriteDeps() {
|
|
|
20702
22053
|
env: process.env,
|
|
20703
22054
|
homedir: os10.homedir,
|
|
20704
22055
|
platform: process.platform,
|
|
20705
|
-
readFile: (p) =>
|
|
20706
|
-
mkdir: (p, options) =>
|
|
20707
|
-
writeFile: (p, data, options) =>
|
|
22056
|
+
readFile: (p) => readFile11(p, "utf-8"),
|
|
22057
|
+
mkdir: (p, options) => mkdir10(p, options),
|
|
22058
|
+
writeFile: (p, data, options) => writeFile10(p, data, options),
|
|
20708
22059
|
rename: (oldPath, newPath) => rename3(oldPath, newPath),
|
|
20709
22060
|
chmod: (p, mode) => chmod3(p, mode),
|
|
20710
22061
|
unlink: (p) => unlink3(p)
|
|
@@ -20761,39 +22112,39 @@ async function getProjectRoot() {
|
|
|
20761
22112
|
var docsDirPromise;
|
|
20762
22113
|
async function getDocsDir() {
|
|
20763
22114
|
if (!docsDirPromise) {
|
|
20764
|
-
docsDirPromise = (async () =>
|
|
22115
|
+
docsDirPromise = (async () => path26.resolve(await getProjectRoot(), process.env.BAPI_DOCS_DIR ?? "docs/tmp"))();
|
|
20765
22116
|
}
|
|
20766
22117
|
return docsDirPromise;
|
|
20767
22118
|
}
|
|
20768
22119
|
var pipelinesDirPromise;
|
|
20769
22120
|
async function getPipelinesDir() {
|
|
20770
22121
|
if (!pipelinesDirPromise) {
|
|
20771
|
-
pipelinesDirPromise = (async () =>
|
|
22122
|
+
pipelinesDirPromise = (async () => path26.resolve(await getProjectRoot(), process.env.BAPI_PIPELINES_DIR ?? ".bridge/pipelines"))();
|
|
20772
22123
|
}
|
|
20773
22124
|
return pipelinesDirPromise;
|
|
20774
22125
|
}
|
|
20775
|
-
function buildUrl(
|
|
20776
|
-
return `${BASE_URL.replace(/\/+$/, "")}/jira${
|
|
22126
|
+
function buildUrl(path27) {
|
|
22127
|
+
return `${BASE_URL.replace(/\/+$/, "")}/jira${path27}`;
|
|
20777
22128
|
}
|
|
20778
|
-
function buildApiUrl(
|
|
20779
|
-
return `${BASE_URL.replace(/\/+$/, "")}${
|
|
22129
|
+
function buildApiUrl(path27) {
|
|
22130
|
+
return `${BASE_URL.replace(/\/+$/, "")}${path27}`;
|
|
20780
22131
|
}
|
|
20781
|
-
function buildGetUrl(
|
|
20782
|
-
const url = new URL(buildUrl(
|
|
22132
|
+
function buildGetUrl(path27, params) {
|
|
22133
|
+
const url = new URL(buildUrl(path27));
|
|
20783
22134
|
for (const [key, value] of Object.entries(params)) {
|
|
20784
22135
|
url.searchParams.set(key, value);
|
|
20785
22136
|
}
|
|
20786
22137
|
return url.toString();
|
|
20787
22138
|
}
|
|
20788
22139
|
async function getDocsPath(subdir) {
|
|
20789
|
-
return
|
|
22140
|
+
return path26.join(await getDocsDir(), subdir);
|
|
20790
22141
|
}
|
|
20791
22142
|
var customPipelinesPromise;
|
|
20792
22143
|
async function ensureCustomPipelinesLoaded() {
|
|
20793
22144
|
if (!customPipelinesPromise) {
|
|
20794
22145
|
customPipelinesPromise = (async () => {
|
|
20795
22146
|
const pipelinesDir = await getPipelinesDir();
|
|
20796
|
-
const instructionsDir =
|
|
22147
|
+
const instructionsDir = path26.join(path26.dirname(pipelinesDir), "instructions");
|
|
20797
22148
|
const customResult = await loadCustomPipelines(
|
|
20798
22149
|
pipelinesDir,
|
|
20799
22150
|
instructionsDir,
|
|
@@ -20878,10 +22229,10 @@ async function createTicketRequest(params) {
|
|
|
20878
22229
|
return handleResponse(resp);
|
|
20879
22230
|
}
|
|
20880
22231
|
async function saveLocally(dir, filename, content) {
|
|
20881
|
-
const filePath =
|
|
22232
|
+
const filePath = path26.join(dir, filename);
|
|
20882
22233
|
try {
|
|
20883
|
-
await
|
|
20884
|
-
await
|
|
22234
|
+
await mkdir10(dir, { recursive: true });
|
|
22235
|
+
await writeFile10(filePath, content, "utf-8");
|
|
20885
22236
|
return `
|
|
20886
22237
|
|
|
20887
22238
|
---
|
|
@@ -20898,19 +22249,19 @@ function safeTimestampForFilename() {
|
|
|
20898
22249
|
return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
20899
22250
|
}
|
|
20900
22251
|
function safeTicketFileSegment(ticketNumber) {
|
|
20901
|
-
const base =
|
|
22252
|
+
const base = path26.basename(ticketNumber.trim());
|
|
20902
22253
|
const cleaned = base.replace(/[^A-Za-z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
|
|
20903
22254
|
return cleaned || "ticket";
|
|
20904
22255
|
}
|
|
20905
22256
|
function isContainedSaveTarget(dir, filename) {
|
|
20906
|
-
const resolvedDir =
|
|
20907
|
-
const target =
|
|
20908
|
-
return target.startsWith(resolvedDir +
|
|
22257
|
+
const resolvedDir = path26.resolve(dir);
|
|
22258
|
+
const target = path26.resolve(resolvedDir, filename);
|
|
22259
|
+
return target.startsWith(resolvedDir + path26.sep);
|
|
20909
22260
|
}
|
|
20910
22261
|
function saveLocallySucceeded(note) {
|
|
20911
22262
|
return note.includes("Saved to ");
|
|
20912
22263
|
}
|
|
20913
|
-
async function
|
|
22264
|
+
async function truncateAndSaveIfNeeded2(text, dir, filename) {
|
|
20914
22265
|
if (text.length <= MAX_INLINE_TEXT_LENGTH) {
|
|
20915
22266
|
return text;
|
|
20916
22267
|
}
|
|
@@ -20967,7 +22318,7 @@ async function resolveTextOrFile(textValue, filePath, textLabel) {
|
|
|
20967
22318
|
}
|
|
20968
22319
|
};
|
|
20969
22320
|
}
|
|
20970
|
-
resolvedText = await
|
|
22321
|
+
resolvedText = await readFile11(filePath, "utf-8");
|
|
20971
22322
|
if (textValue) {
|
|
20972
22323
|
note = `
|
|
20973
22324
|
|
|
@@ -21031,8 +22382,8 @@ async function resolveUploadAttachment(textValue, filePath, textLabel) {
|
|
|
21031
22382
|
}
|
|
21032
22383
|
};
|
|
21033
22384
|
}
|
|
21034
|
-
const ext =
|
|
21035
|
-
const buf = await
|
|
22385
|
+
const ext = path26.extname(filePath).toLowerCase();
|
|
22386
|
+
const buf = await readFile11(filePath);
|
|
21036
22387
|
let isBinary = BINARY_EXTENSIONS.has(ext);
|
|
21037
22388
|
if (!isBinary) {
|
|
21038
22389
|
try {
|
|
@@ -21365,7 +22716,7 @@ Raw body: ${result.text}`
|
|
|
21365
22716
|
}
|
|
21366
22717
|
async function ensurePackageJsonForCliCommand(flagName, cwd) {
|
|
21367
22718
|
try {
|
|
21368
|
-
await stat8(
|
|
22719
|
+
await stat8(path26.join(cwd, "package.json"));
|
|
21369
22720
|
return null;
|
|
21370
22721
|
} catch {
|
|
21371
22722
|
return `Error: No package.json found in current directory.
|
|
@@ -21509,22 +22860,30 @@ var registerTool = ((name, config, handler) => {
|
|
|
21509
22860
|
return toolHandle;
|
|
21510
22861
|
});
|
|
21511
22862
|
var commonFields = {
|
|
21512
|
-
ticket_number:
|
|
21513
|
-
repo_name:
|
|
21514
|
-
save_locally:
|
|
21515
|
-
wait_for_result:
|
|
22863
|
+
ticket_number: z8.string(),
|
|
22864
|
+
repo_name: z8.string().optional(),
|
|
22865
|
+
save_locally: z8.boolean().optional().default(true),
|
|
22866
|
+
wait_for_result: z8.boolean().optional().default(false).describe(
|
|
21516
22867
|
"When true, blocks and polls until ready, returning full content directly. When false (default), returns immediately with confirmation/handle. Use the corresponding get_* tool to retrieve results."
|
|
21517
22868
|
),
|
|
21518
|
-
second_opinion:
|
|
22869
|
+
second_opinion: z8.string().optional().describe(
|
|
21519
22870
|
"Provider routing override for THIS request. NOT the standalone second_opinion tool. Takes precedence over provider."
|
|
21520
22871
|
),
|
|
21521
|
-
provider:
|
|
22872
|
+
provider: z8.string().optional().describe(
|
|
21522
22873
|
"Use a specific LLM provider (openai, anthropic, gemini) without triggering second-opinion semantics."
|
|
21523
22874
|
)
|
|
21524
22875
|
};
|
|
21525
|
-
if (
|
|
22876
|
+
if (ACTIVE_GROUPS.has("conductor")) {
|
|
21526
22877
|
registerConductorTools(registerTool);
|
|
21527
22878
|
}
|
|
22879
|
+
registerSfccTools(registerTool, {
|
|
22880
|
+
buildGetUrl,
|
|
22881
|
+
getGetHeaders,
|
|
22882
|
+
repoName: REPO_NAME,
|
|
22883
|
+
getResolvedApiKey,
|
|
22884
|
+
getDocsDir,
|
|
22885
|
+
includeReadTools: ACTIVE_GROUPS.has("sfcc")
|
|
22886
|
+
});
|
|
21528
22887
|
registerTool(
|
|
21529
22888
|
"ping",
|
|
21530
22889
|
{
|
|
@@ -21573,13 +22932,13 @@ registerTool(
|
|
|
21573
22932
|
},
|
|
21574
22933
|
description: "Use to get an immediate, ad hoc independent critique on a plan or analysis you already have. Returns the responding model's reply text plus the resolved provider. This does NOT create or retrieve a Bridge artifact (use request_* tools for that).",
|
|
21575
22934
|
inputSchema: {
|
|
21576
|
-
prompt:
|
|
22935
|
+
prompt: z8.string().describe(
|
|
21577
22936
|
"The complete, self-contained brief to send to the second-opinion model. Include the full plan, recommendation, analysis, or question you want challenged, plus enough context for the responder to evaluate it independently. This is sent as the user message; the server constructs the system prompt."
|
|
21578
22937
|
),
|
|
21579
|
-
provider:
|
|
22938
|
+
provider: z8.enum(["anthropic", "openai", "gemini"]).describe(
|
|
21580
22939
|
"LLM provider family for the second opinion. Choose a family DIFFERENT from the one you are running on so the response is genuinely independent."
|
|
21581
22940
|
),
|
|
21582
|
-
model:
|
|
22941
|
+
model: z8.enum(["CHEAP_MODEL", "BASIC_MODEL", "PREMIUM_MODEL"]).describe(
|
|
21583
22942
|
"Model tier within the chosen provider. CHEAP_MODEL for quick sanity checks, BASIC_MODEL for focused reviews, PREMIUM_MODEL for serious architectural pushback."
|
|
21584
22943
|
)
|
|
21585
22944
|
}
|
|
@@ -21660,10 +23019,10 @@ registerTool(
|
|
|
21660
23019
|
},
|
|
21661
23020
|
description: "Generate an image from a text prompt using a provider image model. This tool spends provider credits on every call \u2014 cost scales with quality (low/medium/high). Defaults to low quality to minimize provider spend; increase quality only when fidelity matters. Returns native MCP image content (type: 'image') so the caller receives the image directly. The image is always also saved to the local BAPI_DOCS_DIR/images/ directory. Google Imagen outputs (provider='gemini') include an invisible SynthID watermark applied server-side by Google.",
|
|
21662
23021
|
inputSchema: {
|
|
21663
|
-
prompt:
|
|
21664
|
-
provider:
|
|
21665
|
-
quality:
|
|
21666
|
-
size:
|
|
23022
|
+
prompt: z8.string().min(1).max(8e3).describe("Text prompt sent to the image provider."),
|
|
23023
|
+
provider: z8.enum(["openai", "gemini"]).optional().default("openai").describe("Image provider. Defaults to 'openai' (gpt-image-2)."),
|
|
23024
|
+
quality: z8.enum(["low", "medium", "high"]).optional().default("low").describe("Image quality. Defaults to 'low' for cost control."),
|
|
23025
|
+
size: z8.enum(["1024x1024", "1024x1536", "1536x1024"]).optional().default("1024x1024").describe("Image dimensions. Defaults to '1024x1024'.")
|
|
21667
23026
|
}
|
|
21668
23027
|
},
|
|
21669
23028
|
async ({ prompt, provider, quality, size }) => {
|
|
@@ -21757,8 +23116,8 @@ registerTool(
|
|
|
21757
23116
|
const imagesDir = await getDocsPath("images");
|
|
21758
23117
|
const filename = `generated-image-${safeTimestampForFilename()}.png`;
|
|
21759
23118
|
const filePath = `${imagesDir}/${filename}`;
|
|
21760
|
-
await
|
|
21761
|
-
await
|
|
23119
|
+
await mkdir10(imagesDir, { recursive: true });
|
|
23120
|
+
await writeFile10(filePath, Buffer.from(imageBase64, "base64"));
|
|
21762
23121
|
content.push({ type: "text", text: `Saved to ${filePath}` });
|
|
21763
23122
|
} catch (saveErr) {
|
|
21764
23123
|
content.push({
|
|
@@ -21789,7 +23148,7 @@ registerTool(
|
|
|
21789
23148
|
if (ok) {
|
|
21790
23149
|
const safeRepo = safeTicketFileSegment(REPO_NAME || "repo");
|
|
21791
23150
|
const filename = `${safeRepo}-project-standards.md`;
|
|
21792
|
-
text = await
|
|
23151
|
+
text = await truncateAndSaveIfNeeded2(
|
|
21793
23152
|
text,
|
|
21794
23153
|
await getDocsPath("project-standards"),
|
|
21795
23154
|
filename
|
|
@@ -21809,16 +23168,16 @@ registerTool(
|
|
|
21809
23168
|
},
|
|
21810
23169
|
description: "Search for and list Jira tickets from the configured project. Filters by query text, status name, label, or date. Returns up to 'limit' tickets ordered by most recently updated. All data is fetched live from Jira. Use get_ticket to retrieve full details for a specific ticket.",
|
|
21811
23170
|
inputSchema: {
|
|
21812
|
-
query:
|
|
23171
|
+
query: z8.string().optional().describe(
|
|
21813
23172
|
`Free-text search string. Filters tickets via JQL text ~ '...' (searches summary, description, comments). Examples: "authentication error", "login page crash", "payment timeout"`
|
|
21814
23173
|
),
|
|
21815
|
-
status:
|
|
21816
|
-
labels:
|
|
23174
|
+
status: z8.string().optional().describe("Filter by Jira status name (e.g. 'To Do', 'In Progress', 'Done')"),
|
|
23175
|
+
labels: z8.string().optional().describe(
|
|
21817
23176
|
'Comma-separated Jira labels. Filters tickets via JQL labels in (...) (matches tickets carrying any of the given labels). Labels cannot contain spaces. Example: "bapi-idea-to-ticket-fa-1a2b3c"'
|
|
21818
23177
|
),
|
|
21819
|
-
limit:
|
|
21820
|
-
offset:
|
|
21821
|
-
updated_since:
|
|
23178
|
+
limit: z8.number().optional().default(20).describe("Maximum number of tickets to return (1-100, default 20)"),
|
|
23179
|
+
offset: z8.number().optional().default(0).describe("Number of results to skip for pagination (default 0)"),
|
|
23180
|
+
updated_since: z8.string().optional().describe("ISO date string (YYYY-MM-DD). Only return tickets updated on or after this date")
|
|
21822
23181
|
}
|
|
21823
23182
|
},
|
|
21824
23183
|
async ({ query, status, labels, limit, offset, updated_since }) => {
|
|
@@ -21842,7 +23201,7 @@ registerTool(
|
|
|
21842
23201
|
limit,
|
|
21843
23202
|
offset
|
|
21844
23203
|
});
|
|
21845
|
-
text = await
|
|
23204
|
+
text = await truncateAndSaveIfNeeded2(
|
|
21846
23205
|
text,
|
|
21847
23206
|
await getDocsPath("tickets-search"),
|
|
21848
23207
|
filename
|
|
@@ -21876,7 +23235,7 @@ registerTool(
|
|
|
21876
23235
|
if (ok) {
|
|
21877
23236
|
const safeTicket = safeTicketFileSegment(ticket_number);
|
|
21878
23237
|
const filename = `${safeTicket}.json`;
|
|
21879
|
-
text = await
|
|
23238
|
+
text = await truncateAndSaveIfNeeded2(
|
|
21880
23239
|
text,
|
|
21881
23240
|
await getDocsPath("tickets"),
|
|
21882
23241
|
filename
|
|
@@ -21934,7 +23293,7 @@ registerTool(
|
|
|
21934
23293
|
if (ok) {
|
|
21935
23294
|
const safeTicket = safeTicketFileSegment(ticket_number);
|
|
21936
23295
|
const filename = `${safeTicket}-comments.json`;
|
|
21937
|
-
text = await
|
|
23296
|
+
text = await truncateAndSaveIfNeeded2(
|
|
21938
23297
|
text,
|
|
21939
23298
|
await getDocsPath("comments"),
|
|
21940
23299
|
filename
|
|
@@ -21954,18 +23313,18 @@ registerTool(
|
|
|
21954
23313
|
},
|
|
21955
23314
|
description: "Create a new Jira ticket in the configured project. Requires either description or file_path (or both \u2014 file_path takes precedence). Returns JSON with {ticket_key: 'PROJ-123', url: 'https://...'}. The ticket is created immediately in Jira \u2014 confirm details with the user before calling. The description field supports Jira markdown formatting. Pass parent_key ONLY when creating a child ticket under an existing Jira Epic; omit it for standalone tickets and for Epic parent creation itself.",
|
|
21956
23315
|
inputSchema: {
|
|
21957
|
-
summary:
|
|
21958
|
-
description:
|
|
23316
|
+
summary: z8.string().describe("Ticket title \u2014 keep under 100 characters"),
|
|
23317
|
+
description: z8.string().optional().describe(
|
|
21959
23318
|
"Required unless file_path is provided. Detailed description in markdown. Recommended structure: Summary (2-4 sentences), Requirements (bullet list with code file references), Acceptance Criteria (testable 'Done when...' statements)"
|
|
21960
23319
|
),
|
|
21961
|
-
file_path:
|
|
23320
|
+
file_path: z8.string().optional().describe(
|
|
21962
23321
|
"Path to a local markdown file whose contents will be used as the ticket description. If both file_path and description are provided, file_path takes precedence. The file must be UTF-8 encoded and under 1MB."
|
|
21963
23322
|
),
|
|
21964
|
-
issue_type:
|
|
21965
|
-
priority:
|
|
21966
|
-
labels:
|
|
21967
|
-
assignee:
|
|
21968
|
-
parent_key:
|
|
23323
|
+
issue_type: z8.string().describe("One of: 'Bug' (defect), 'Story' (user-facing feature), 'Task' (technical/infrastructure work)"),
|
|
23324
|
+
priority: z8.string().optional().describe("One of: 'Highest', 'High', 'Medium', 'Low', 'Lowest'. Omit to use Jira project default"),
|
|
23325
|
+
labels: z8.array(z8.string()).optional().describe("List of Jira labels to apply (e.g. ['frontend', 'tech-debt'])"),
|
|
23326
|
+
assignee: z8.string().optional().describe("Jira username or account ID of the assignee. Omit to leave unassigned"),
|
|
23327
|
+
parent_key: z8.string().optional().describe(
|
|
21969
23328
|
"Optional Jira Epic key to set as the parent of the newly created child issue. Omit for standalone tickets and Epic parent creation."
|
|
21970
23329
|
)
|
|
21971
23330
|
}
|
|
@@ -22064,7 +23423,7 @@ registerTool(
|
|
|
22064
23423
|
},
|
|
22065
23424
|
description: "Queue a background job to parse and index the repository for Bridge API's AI agents. This should be run after major codebase changes so that plans and questions reflect the latest code. Returns 202 with {message: 'Repository parsing queued'} on success, or {message: 'Repository parsing already in progress'} if a job is already running. The job runs asynchronously \u2014 there is no completion callback. For large repositories this may take several minutes. Confirm with the user before triggering.",
|
|
22066
23425
|
inputSchema: {
|
|
22067
|
-
directory_path:
|
|
23426
|
+
directory_path: z8.string().optional().describe(
|
|
22068
23427
|
"Subdirectory to scope the parse to (e.g. 'src/python'). Omit to parse the entire repository"
|
|
22069
23428
|
)
|
|
22070
23429
|
}
|
|
@@ -22141,14 +23500,14 @@ registerTool(
|
|
|
22141
23500
|
description: "Post a comment on a Jira ticket. The comment appears immediately in Jira. Supports markdown formatting. For long comments (over ~2000 characters), set attach_as_file to true \u2014 this attaches the comment as a .md file instead of posting inline, which avoids Jira's comment length limitations.\n\nTip: To generate plans, clarifying questions, or ticket critiques, use the dedicated request_plan_generation, request_clarifying_questions, or request_ticket_critique tools.",
|
|
22142
23501
|
inputSchema: {
|
|
22143
23502
|
ticket_number: commonFields.ticket_number,
|
|
22144
|
-
comment:
|
|
22145
|
-
file_path:
|
|
23503
|
+
comment: z8.string().optional().describe("Comment text in markdown format. Can include code blocks, lists, headings, etc. Optional if file_path is provided."),
|
|
23504
|
+
file_path: z8.string().optional().describe(
|
|
22146
23505
|
"Path to a local markdown file whose contents will be used as the comment. If both file_path and comment are provided, file_path takes precedence. The file must be UTF-8 encoded and under 1MB."
|
|
22147
23506
|
),
|
|
22148
|
-
attach_as_file:
|
|
23507
|
+
attach_as_file: z8.boolean().optional().default(false).describe(
|
|
22149
23508
|
"Set to true to attach the comment as a .md file instead of posting inline. Recommended for comments over 2000 characters"
|
|
22150
23509
|
),
|
|
22151
|
-
file_name:
|
|
23510
|
+
file_name: z8.string().optional().describe(
|
|
22152
23511
|
"Custom filename for the attached .md file (only used when attach_as_file is true). Defaults to {ticket_number}-comment.md if not provided. Example: 'PROJ-123-clarifying-questions.md'"
|
|
22153
23512
|
)
|
|
22154
23513
|
}
|
|
@@ -22188,8 +23547,8 @@ registerTool(
|
|
|
22188
23547
|
description: "Update the description of an existing Jira ticket. This is a direct, synchronous update that overwrites the existing description with the provided text. The description should be in markdown format \u2014 it will be automatically converted to Jira wiki markup. This does NOT create a new ticket. Use create_ticket for that. Returns a success message with the ticket number, or an error if the update fails.",
|
|
22189
23548
|
inputSchema: {
|
|
22190
23549
|
ticket_number: commonFields.ticket_number,
|
|
22191
|
-
description:
|
|
22192
|
-
file_path:
|
|
23550
|
+
description: z8.string().optional().describe("New description text in markdown format. Optional if file_path is provided. This will completely replace the existing description."),
|
|
23551
|
+
file_path: z8.string().optional().describe(
|
|
22193
23552
|
"Path to a local markdown file whose contents will be used as the new description. If both file_path and description are provided, file_path takes precedence. The file must be UTF-8 encoded and under 1MB."
|
|
22194
23553
|
)
|
|
22195
23554
|
}
|
|
@@ -22219,35 +23578,35 @@ registerTool(
|
|
|
22219
23578
|
openWorldHint: true
|
|
22220
23579
|
},
|
|
22221
23580
|
description: "Manages Jira attachments. Operations: upload, download, list.",
|
|
22222
|
-
inputSchema:
|
|
22223
|
-
|
|
22224
|
-
operation:
|
|
23581
|
+
inputSchema: z8.discriminatedUnion("operation", [
|
|
23582
|
+
z8.object({
|
|
23583
|
+
operation: z8.literal("upload"),
|
|
22225
23584
|
ticket_number: commonFields.ticket_number,
|
|
22226
|
-
file_path:
|
|
23585
|
+
file_path: z8.string().optional().describe(
|
|
22227
23586
|
"Path to a local file to upload. Binary files up to `10 MB`; text up to `1 MB`. If both file_path and content are provided, file_path takes precedence."
|
|
22228
23587
|
),
|
|
22229
|
-
content:
|
|
22230
|
-
file_name:
|
|
23588
|
+
content: z8.string().max(1048576).optional().describe("Inline text content to upload (max `1 MB`). Optional if file_path is provided."),
|
|
23589
|
+
file_name: z8.string().optional().describe(
|
|
22231
23590
|
"Filename for the attachment in Jira. Defaults to the basename of file_path if provided, or {ticket_number}-attachment.md otherwise."
|
|
22232
23591
|
),
|
|
22233
|
-
link_type:
|
|
23592
|
+
link_type: z8.string().optional().describe(
|
|
22234
23593
|
"When provided, also syncs the content to Bridge API's tickets_links table. Known values: clarifying-questions.md, debugging-guidance.md, ticket-quality-critique.md, architecture-plan.md, fsd-plan.md, prd-plan.md. Cannot be used with binary file uploads."
|
|
22235
23594
|
),
|
|
22236
|
-
replace_existing:
|
|
23595
|
+
replace_existing: z8.boolean().optional().default(true).describe(
|
|
22237
23596
|
"When true (default), deletes any existing attachment with the same filename before uploading."
|
|
22238
23597
|
)
|
|
22239
23598
|
}).strict(),
|
|
22240
|
-
|
|
22241
|
-
operation:
|
|
23599
|
+
z8.object({
|
|
23600
|
+
operation: z8.literal("download"),
|
|
22242
23601
|
ticket_number: commonFields.ticket_number,
|
|
22243
|
-
attachment_id:
|
|
22244
|
-
filename:
|
|
22245
|
-
file_path:
|
|
23602
|
+
attachment_id: z8.string().optional().describe("Jira attachment ID. Mutually exclusive with filename."),
|
|
23603
|
+
filename: z8.string().optional().describe("Attachment filename. If multiple exist, returns the most recent. Mutually exclusive with attachment_id."),
|
|
23604
|
+
file_path: z8.string().optional().describe("Override the default save location. If omitted, saves to {BAPI_DOCS_DIR}/attachments/{ticket_number}/{filename}.")
|
|
22246
23605
|
}).strict(),
|
|
22247
|
-
|
|
22248
|
-
operation:
|
|
23606
|
+
z8.object({
|
|
23607
|
+
operation: z8.literal("list"),
|
|
22249
23608
|
ticket_number: commonFields.ticket_number,
|
|
22250
|
-
include_ai_generated:
|
|
23609
|
+
include_ai_generated: z8.boolean().optional().describe("Include AI-generated attachments in the list (default: false)")
|
|
22251
23610
|
}).strict()
|
|
22252
23611
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
22253
23612
|
])
|
|
@@ -22259,7 +23618,7 @@ registerTool(
|
|
|
22259
23618
|
const { ticket_number, file_path, content, file_name, link_type, replace_existing } = args;
|
|
22260
23619
|
const resolved = await resolveUploadAttachment(content, file_path, "content");
|
|
22261
23620
|
if (!resolved.ok) return resolved.errorResponse;
|
|
22262
|
-
const derivedFileName = file_name || (file_path ?
|
|
23621
|
+
const derivedFileName = file_name || (file_path ? path26.basename(file_path) : `${ticket_number}-attachment.md`);
|
|
22263
23622
|
const payload = {
|
|
22264
23623
|
repo_name: REPO_NAME,
|
|
22265
23624
|
content: resolved.text,
|
|
@@ -22314,12 +23673,12 @@ registerTool(
|
|
|
22314
23673
|
const isText = body.is_text;
|
|
22315
23674
|
const mimeType = body.mime_type;
|
|
22316
23675
|
const size = body.size;
|
|
22317
|
-
const safeFileName =
|
|
22318
|
-
const safeTicket =
|
|
22319
|
-
const savePath = file_path ? file_path :
|
|
22320
|
-
const resolvedSave =
|
|
22321
|
-
const resolvedRoot =
|
|
22322
|
-
if (!resolvedSave.startsWith(resolvedRoot +
|
|
23676
|
+
const safeFileName = path26.basename(serverFilename);
|
|
23677
|
+
const safeTicket = path26.basename(ticket_number);
|
|
23678
|
+
const savePath = file_path ? file_path : path26.join(await getDocsDir(), "attachments", safeTicket, safeFileName);
|
|
23679
|
+
const resolvedSave = path26.resolve(savePath);
|
|
23680
|
+
const resolvedRoot = path26.resolve(await getProjectRoot());
|
|
23681
|
+
if (!resolvedSave.startsWith(resolvedRoot + path26.sep) && resolvedSave !== resolvedRoot) {
|
|
22323
23682
|
return {
|
|
22324
23683
|
content: [{
|
|
22325
23684
|
type: "text",
|
|
@@ -22330,11 +23689,11 @@ registerTool(
|
|
|
22330
23689
|
}]
|
|
22331
23690
|
};
|
|
22332
23691
|
}
|
|
22333
|
-
await
|
|
23692
|
+
await mkdir10(path26.dirname(resolvedSave), { recursive: true });
|
|
22334
23693
|
if (isText) {
|
|
22335
|
-
await
|
|
23694
|
+
await writeFile10(resolvedSave, content, "utf-8");
|
|
22336
23695
|
} else {
|
|
22337
|
-
await
|
|
23696
|
+
await writeFile10(resolvedSave, Buffer.from(content, "base64"));
|
|
22338
23697
|
}
|
|
22339
23698
|
let resultText = `File saved to: ${resolvedSave}
|
|
22340
23699
|
Filename: ${safeFileName}
|
|
@@ -22371,7 +23730,7 @@ ${content}`;
|
|
|
22371
23730
|
if (ok) {
|
|
22372
23731
|
const safeTicket = safeTicketFileSegment(ticket_number);
|
|
22373
23732
|
const fname = `${safeTicket}-attachment-list.json`;
|
|
22374
|
-
text = await
|
|
23733
|
+
text = await truncateAndSaveIfNeeded2(
|
|
22375
23734
|
text,
|
|
22376
23735
|
await getDocsPath("attachments"),
|
|
22377
23736
|
fname
|
|
@@ -22464,15 +23823,15 @@ registerTool(
|
|
|
22464
23823
|
description: "Use to start async generation of a design document (tdd, fsd, or prd) for a Jira ticket. Returns confirmation immediately (or the full document if wait_for_result is true). Use get_doc to retrieve. Generates and persists a retrievable artifact.",
|
|
22465
23824
|
inputSchema: {
|
|
22466
23825
|
ticket_number: commonFields.ticket_number,
|
|
22467
|
-
doc_type:
|
|
23826
|
+
doc_type: z8.enum(["tdd", "fsd", "prd"]).describe(
|
|
22468
23827
|
"Which design document to generate: 'tdd' (Technical Design Document, engineer audience), 'fsd' (Functional Specification Document, product/functional audience), or 'prd' (Product Requirements Document, product-requirements focused: problem, goals, success metrics)."
|
|
22469
23828
|
),
|
|
22470
23829
|
wait_for_result: commonFields.wait_for_result,
|
|
22471
23830
|
save_locally: commonFields.save_locally,
|
|
22472
|
-
second_opinion:
|
|
23831
|
+
second_opinion: z8.string().optional().describe(
|
|
22473
23832
|
"Provider routing override for THIS artifact-generation request (e.g. 'anthropic', 'openai', 'gemini'). When set, the artifact is generated by the named provider and, where supported, a cross-provider second-opinion pass is applied to this request only. Takes precedence over `provider` when both are set."
|
|
22474
23833
|
),
|
|
22475
|
-
provider:
|
|
23834
|
+
provider: z8.string().optional().describe(
|
|
22476
23835
|
"Pure provider switch \u2014 use a specific LLM provider (openai, anthropic, gemini) without triggering second-opinion semantics. If both provider and second_opinion are set, second_opinion takes precedence."
|
|
22477
23836
|
)
|
|
22478
23837
|
}
|
|
@@ -22493,7 +23852,7 @@ registerTool(
|
|
|
22493
23852
|
description: "RETRIEVE an already-generated design document for a Jira ticket, routed by doc_type. Use doc_type 'tdd' for the Technical Design Document, 'fsd' for the Functional Specification Document, or 'prd' for the Product Requirements Document. This tool only fetches an existing document \u2014 it does NOT start or trigger generation. If no document exists yet (or you need a fresh one), call `create_doc` first with the same doc_type. Returns the full document as markdown text \u2014 present it verbatim without summarizing. Returns a 404 / not-found response when no document is ready yet \u2014 that means generation has not run, not that this tool failed.",
|
|
22494
23853
|
inputSchema: {
|
|
22495
23854
|
ticket_number: commonFields.ticket_number,
|
|
22496
|
-
doc_type:
|
|
23855
|
+
doc_type: z8.enum(["tdd", "fsd", "prd"]).describe(
|
|
22497
23856
|
"Which design document to retrieve: 'tdd' (Technical Design Document), 'fsd' (Functional Specification Document), or 'prd' (Product Requirements Document)."
|
|
22498
23857
|
),
|
|
22499
23858
|
save_locally: commonFields.save_locally
|
|
@@ -22641,7 +24000,7 @@ registerTool(
|
|
|
22641
24000
|
description: "Write/update Bridge API's DATABASE lifecycle-tracking record for a ticket ONLY. This registers the ticket in Bridge's own database so workflow state timestamps (critique, clarify, plan, implement) can be tracked. It does NOT edit anything in Jira: it does not change the Jira summary, description, comments, attachments, or status. If the ticket is already tracked, this is a safe no-op \u2014 it upserts the description and repo_name without error. After create_ticket, this is the correct next step when you want Bridge to track that ticket's workflow timestamps / artifact state. For Jira mutations use a different tool instead: `update_ticket_description` to replace the Jira description, `add_comment` to post a Jira comment, and `update_jira_status` to move the Jira workflow status. The repo_name is automatically injected from the configured environment.",
|
|
22642
24001
|
inputSchema: {
|
|
22643
24002
|
ticket_number: commonFields.ticket_number,
|
|
22644
|
-
description:
|
|
24003
|
+
description: z8.string().optional().describe("Ticket description text. Optional \u2014 used to store a local copy of the description for reference.")
|
|
22645
24004
|
}
|
|
22646
24005
|
},
|
|
22647
24006
|
async ({ ticket_number, description }) => {
|
|
@@ -22671,7 +24030,7 @@ registerTool(
|
|
|
22671
24030
|
description: "Update workflow state timestamps on a tracked ticket. Each specified field is set to the current UTC timestamp on the server. Valid field names: 'critique_called', 'critique_answered', 'clarify_called', 'clarify_answered', 'plan_generated', 'implemented', 'reimplement_called'. The ticket must already be tracked (via track_ticket) or a 404 error is returned. Returns 400 if any field name is invalid. The repo_name is automatically injected from the configured environment.",
|
|
22672
24031
|
inputSchema: {
|
|
22673
24032
|
ticket_number: commonFields.ticket_number,
|
|
22674
|
-
fields:
|
|
24033
|
+
fields: z8.array(z8.string()).describe("List of state field names to set to the current UTC timestamp. Valid values: 'critique_called', 'critique_answered', 'clarify_called', 'clarify_answered', 'plan_generated', 'implemented', 'reimplement_called'")
|
|
22675
24034
|
}
|
|
22676
24035
|
},
|
|
22677
24036
|
async ({ ticket_number, fields }) => {
|
|
@@ -22748,8 +24107,8 @@ registerTool(
|
|
|
22748
24107
|
description: 'Transition a Jira ticket to a specified target status by executing a workflow transition. Provide either target_status (matched case-insensitively against available transitions) or transition_id (used directly). If transition_id is provided, it takes precedence over target_status. Pass target_status as "auto" to trigger server-side status resolution via LLM \u2014 the server determines the correct post-PR status automatically. If auto-resolve finds no match, returns status: skipped (not an error). Returns the from/to status on success, or an error listing available transitions if no match is found. The repo_name is automatically injected from the configured environment.',
|
|
22749
24108
|
inputSchema: {
|
|
22750
24109
|
ticket_number: commonFields.ticket_number,
|
|
22751
|
-
target_status:
|
|
22752
|
-
transition_id:
|
|
24110
|
+
target_status: z8.string().optional().describe('Target status name to transition to (case-insensitive match). Pass "auto" to resolve the target status server-side via LLM agent.'),
|
|
24111
|
+
transition_id: z8.string().optional().describe("Specific transition ID to execute (takes precedence over target_status)")
|
|
22753
24112
|
}
|
|
22754
24113
|
},
|
|
22755
24114
|
async ({ ticket_number, target_status, transition_id }) => {
|
|
@@ -22780,7 +24139,7 @@ registerTool(
|
|
|
22780
24139
|
description: "Ask an LLM agent to CHOOSE the project's post-PR target Jira status, and cache that choice per project. The agent selects the single workflow status that best represents 'code committed via PR but not yet tested.' Results are cached per-project \u2014 subsequent calls return the cached value unless force_rerun is true. This does NOT list all available transitions \u2014 use `get_jira_transitions` for the full transition list. This also does NOT move the ticket \u2014 use `update_jira_status` to actually perform the status transition. Requires a ticket_number to fetch available transitions from Jira. The repo_name is automatically injected from the configured environment.",
|
|
22781
24140
|
inputSchema: {
|
|
22782
24141
|
ticket_number: commonFields.ticket_number,
|
|
22783
|
-
force_rerun:
|
|
24142
|
+
force_rerun: z8.boolean().optional().describe("Set to true to bypass the cache and re-resolve the target status via LLM")
|
|
22784
24143
|
}
|
|
22785
24144
|
},
|
|
22786
24145
|
async ({ ticket_number, force_rerun }) => {
|
|
@@ -22845,35 +24204,35 @@ registerTool(
|
|
|
22845
24204
|
openWorldHint: true
|
|
22846
24205
|
},
|
|
22847
24206
|
description: "Manages Bridge API configuration fields. Operations: get, update, list.",
|
|
22848
|
-
inputSchema:
|
|
22849
|
-
|
|
22850
|
-
operation:
|
|
22851
|
-
field_name:
|
|
24207
|
+
inputSchema: z8.discriminatedUnion("operation", [
|
|
24208
|
+
z8.object({
|
|
24209
|
+
operation: z8.literal("get"),
|
|
24210
|
+
field_name: z8.string().describe(
|
|
22852
24211
|
`Read the current value and metadata for a config field. For install bootstrap, prefer get_install_manifest over many individual reads. Valid options: ${VALID_CONFIG_FIELDS}`
|
|
22853
24212
|
)
|
|
22854
24213
|
}).strict(),
|
|
22855
|
-
|
|
22856
|
-
operation:
|
|
22857
|
-
field_name:
|
|
24214
|
+
z8.object({
|
|
24215
|
+
operation: z8.literal("update"),
|
|
24216
|
+
field_name: z8.string().describe(
|
|
22858
24217
|
`The configuration field to update. Valid options: ${VALID_CONFIG_FIELDS}. Always call with operation: "get" first to read the current value. For install bootstrap, prefer apply_install_manifest over many individual updates. Returns 400 if the field name is invalid, 404 if no configuration row exists.`
|
|
22859
24218
|
),
|
|
22860
|
-
value:
|
|
22861
|
-
|
|
22862
|
-
|
|
22863
|
-
|
|
22864
|
-
|
|
24219
|
+
value: z8.union([
|
|
24220
|
+
z8.string(),
|
|
24221
|
+
z8.boolean(),
|
|
24222
|
+
z8.array(z8.string()),
|
|
24223
|
+
z8.record(z8.string(), z8.union([z8.string(), z8.null()]))
|
|
22865
24224
|
]).optional().describe(
|
|
22866
24225
|
`The new value for the configuration field. Provide either value or file_path, not both. Most fields take a string; scalar boolean fields (e.g. allow_mutating_smoke_ops, difficulty_model_routing_enabled) take true/false. The selected_mcp_slugs field takes a JSON array of supported MCP validation manual slug strings (e.g. ["b2c-commerce-developer", "playwright-mcp", "pwa-kit-mcp"]) \u2014 pass an array of strings, not a comma-delimited string; an empty array clears the selection. The difficulty_model_tier_overrides field takes a JSON object mapping tier names ("cheap"/"basic"/"premium") to per-repo model aliases (e.g. {"premium": "opus"}) \u2014 pass an object, not a string; an empty object clears all overrides. The difficulty_model_routing_enabled field enables difficulty-based /start-tickets model routing (default ON); pass true/false. The base_branch field is a string/null field controlling the development base branch used by PR creation (/create-pr) and start-tickets worktree creation; an empty/null value clears it and automations fall back to 'main'. For string fields, omit both value and file_path to set the field to NULL (clearing it). Scalar boolean fields are NOT NULL and have no clear/null state: omitting the value writes false (matching the API-layer coercion), so pass true/false explicitly.`
|
|
22867
24226
|
),
|
|
22868
|
-
file_path:
|
|
24227
|
+
file_path: z8.string().optional().describe(
|
|
22869
24228
|
"Path to a local file whose contents will be used as the new value. Useful for large configuration values like detailed review instructions. The file must be UTF-8 encoded and under 1MB. Not supported for scalar boolean fields like allow_mutating_smoke_ops."
|
|
22870
24229
|
),
|
|
22871
|
-
only_if_null:
|
|
24230
|
+
only_if_null: z8.boolean().optional().describe(
|
|
22872
24231
|
"Secondary conditional-write guard: when true, the field is updated only if its column is currently NULL (returns status 'skipped'/reason 'already_set' otherwise). Legal only for nullable columns (HTTP 422 otherwise). For easy install, prefer apply_install_manifest."
|
|
22873
24232
|
)
|
|
22874
24233
|
}).strict(),
|
|
22875
|
-
|
|
22876
|
-
operation:
|
|
24234
|
+
z8.object({
|
|
24235
|
+
operation: z8.literal("list")
|
|
22877
24236
|
}).strict()
|
|
22878
24237
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
22879
24238
|
])
|
|
@@ -23083,10 +24442,10 @@ registerTool(
|
|
|
23083
24442
|
},
|
|
23084
24443
|
description: 'Apply easy-install configuration in one atomic call. Pass the snapshot_token returned by get_install_manifest plus a fields object. Each field value is either a scalar (e.g. "base_branch": "main") or an object (e.g. "project_description": {"value": "...", "confirmed": true}). project_description MUST be passed as {value, confirmed: true} and only after explicit human approval. The server owns skip-if-set, conflict detection, and confirmation semantics and returns six buckets: applied, skipped, conflict, rejected, deferred, needs_confirmation. Only bootstrap-eligible fields are accepted (others are rejected).',
|
|
23085
24444
|
inputSchema: {
|
|
23086
|
-
snapshot_token:
|
|
24445
|
+
snapshot_token: z8.string().describe(
|
|
23087
24446
|
"The exact snapshot_token returned by get_install_manifest for this repository."
|
|
23088
24447
|
),
|
|
23089
|
-
fields:
|
|
24448
|
+
fields: z8.record(z8.string(), z8.any()).describe(
|
|
23090
24449
|
'Map of field_name to value. A value is either a scalar or an object {value, confirmed}. Pass project_description only as {value: "...", confirmed: true} after human approval.'
|
|
23091
24450
|
)
|
|
23092
24451
|
}
|
|
@@ -23100,7 +24459,25 @@ registerTool(
|
|
|
23100
24459
|
body: JSON.stringify({ repo_name: REPO_NAME, snapshot_token, fields })
|
|
23101
24460
|
}
|
|
23102
24461
|
);
|
|
23103
|
-
|
|
24462
|
+
let text = await handleResponse(resp);
|
|
24463
|
+
try {
|
|
24464
|
+
const wiResp = await fetch(
|
|
24465
|
+
buildGetUrl("/config-field/working_in", { repo_name: REPO_NAME }),
|
|
24466
|
+
{ headers: await getGetHeaders() }
|
|
24467
|
+
);
|
|
24468
|
+
if (wiResp.ok) {
|
|
24469
|
+
const wiBody = await wiResp.json();
|
|
24470
|
+
if (wiBody.value === "Salesforce Commerce Cloud") {
|
|
24471
|
+
const newProfile = await mergeBridgeApiProfileToken(await getProjectRoot(), "sfcc");
|
|
24472
|
+
if (newProfile) {
|
|
24473
|
+
text += `
|
|
24474
|
+
|
|
24475
|
+
\u26A0\uFE0F SFCC profile updated: BRIDGE_MCP_PROFILE is now set to \`${newProfile}\` in your local MCP config file(s). This activates on the next MCP server launch \u2014 restart your MCP client to gain access to the SFCC read tools.`;
|
|
24476
|
+
}
|
|
24477
|
+
}
|
|
24478
|
+
}
|
|
24479
|
+
} catch {
|
|
24480
|
+
}
|
|
23104
24481
|
return { content: [{ type: "text", text }] };
|
|
23105
24482
|
}
|
|
23106
24483
|
);
|
|
@@ -23115,7 +24492,7 @@ registerTool(
|
|
|
23115
24492
|
},
|
|
23116
24493
|
description: "Persist the ALREADY-VALIDATED Bridge API key for this repo into the user-scoped credential store (`~/.config/bridge/credentials.json`) under the target `bapi:<repo_name>`, so that Bash-spawned CLI features such as `start-tickets` (a different runtime surface than the MCP server) can resolve it for difficulty\u2192model routing. This is the final stage of `/install-bridge`. The key is resolved INSIDE the MCP server process (env-first, then the existing store) using the provided `repo_name` as the store identity \u2014 it is NEVER passed as a tool argument. Existing credentials are preserved; only `BAPI_API_KEY` for this repo is upserted. The response is secret-free (it reports ok/action/target/path only) and never echoes the key value.",
|
|
23117
24494
|
inputSchema: {
|
|
23118
|
-
repo_name:
|
|
24495
|
+
repo_name: z8.string().describe(
|
|
23119
24496
|
"The repository name to store the routing credential under (target `bapi:<repo_name>`). This is the ONLY input \u2014 do not pass the API key, a secret, or a token; the key is resolved inside the MCP server process."
|
|
23120
24497
|
)
|
|
23121
24498
|
}
|
|
@@ -23250,10 +24627,10 @@ registerTool(
|
|
|
23250
24627
|
},
|
|
23251
24628
|
description: "Use to start async deep research on a technical topic using AI-powered web search. Returns a task_id immediately (or the full report if wait_for_result is true). Use get_deep_research to retrieve. Generates and persists a retrievable artifact.",
|
|
23252
24629
|
inputSchema: {
|
|
23253
|
-
query:
|
|
24630
|
+
query: z8.string().describe(
|
|
23254
24631
|
"The research query. Be specific and detailed about what you need to learn. Good: 'What are the tradeoffs between Redis, Memcached, and DynamoDB DAX for caching in a Python FastAPI application serving 10k RPM, including connection pooling, serialization overhead, and failure modes?' Bad: 'caching options' (too vague \u2014 use a web search instead)"
|
|
23255
24632
|
),
|
|
23256
|
-
context:
|
|
24633
|
+
context: z8.string().optional().describe(
|
|
23257
24634
|
"Optional context to focus the research scope. Describe your current task, tech stack, and constraints. Example: 'I am building a FastAPI application that uses PostgreSQL and needs to implement real-time notifications. Focus on Python-specific solutions compatible with async frameworks.'"
|
|
23258
24635
|
),
|
|
23259
24636
|
ticket_number: commonFields.ticket_number.optional(),
|
|
@@ -23348,10 +24725,10 @@ registerTool(
|
|
|
23348
24725
|
},
|
|
23349
24726
|
description: "RETRIEVE the result of a previously submitted deep research request. This tool only fetches an existing/in-progress result \u2014 it does NOT start or trigger new research. If you have not submitted a research request yet (or you need a new one), call `request_deep_research` first; it starts the async research and this `get_deep_research` tool retrieves the result. Returns the full markdown research report if the task is completed, or a structured status response (still processing / failed / not-found) if the report is not ready yet \u2014 that means research has not finished, not that this tool failed. Use this after calling request_deep_research with wait_for_result=false.",
|
|
23350
24727
|
inputSchema: {
|
|
23351
|
-
task_id:
|
|
24728
|
+
task_id: z8.number().describe(
|
|
23352
24729
|
"The task ID returned by request_deep_research."
|
|
23353
24730
|
),
|
|
23354
|
-
query_slug:
|
|
24731
|
+
query_slug: z8.string().optional().describe(
|
|
23355
24732
|
"Optional slug derived from the original query, used for the saved filename. If omitted, the file is saved as 'research-{task_id}.md'."
|
|
23356
24733
|
),
|
|
23357
24734
|
save_locally: commonFields.save_locally
|
|
@@ -23473,26 +24850,26 @@ registerTool(
|
|
|
23473
24850
|
},
|
|
23474
24851
|
description: "Use to start an async brainstorm that fans out a task to opinion-provider LLMs. Returns a brainstorm_id immediately (or the full result envelope if wait_for_result is true). Use get_brainstorm to retrieve. Generates and persists a retrievable artifact.",
|
|
23475
24852
|
inputSchema: {
|
|
23476
|
-
task_description:
|
|
24853
|
+
task_description: z8.string().describe(
|
|
23477
24854
|
"Free-form description of the task to brainstorm about. Sent verbatim \u2014 this tool does NOT read task_description from a file."
|
|
23478
24855
|
),
|
|
23479
24856
|
repo_name: commonFields.repo_name,
|
|
23480
24857
|
ticket_number: commonFields.ticket_number.optional(),
|
|
23481
|
-
providers:
|
|
24858
|
+
providers: z8.array(z8.string()).optional().describe(
|
|
23482
24859
|
"Opinion-provider LLMs. Defaults to ['openai', 'gemini']. A single-provider request runs one opinion provider and returns that provider's markdown directly."
|
|
23483
24860
|
),
|
|
23484
|
-
concerns:
|
|
24861
|
+
concerns: z8.string().optional().describe(
|
|
23485
24862
|
"Optional caller-supplied concerns to surface to the brainstorm agents."
|
|
23486
24863
|
),
|
|
23487
24864
|
wait_for_result: commonFields.wait_for_result,
|
|
23488
24865
|
save_locally: commonFields.save_locally,
|
|
23489
|
-
prior_brainstorm_id:
|
|
24866
|
+
prior_brainstorm_id: z8.string().optional().describe(
|
|
23490
24867
|
"Optional brainstorm_id from an earlier brainstorm to refine. When provided, the prior brainstorm's completed opinion-provider markdowns are concatenated and supplied as prior context."
|
|
23491
24868
|
),
|
|
23492
|
-
mode:
|
|
24869
|
+
mode: z8.enum(["technical", "design", "discovery"]).optional().describe(
|
|
23493
24870
|
"Preferred brainstorm-mode selector for new callers. 'technical' (default) is the implementation/architecture brainstorm; 'design' is web-page/UI visual-direction ideation; 'discovery' generates grouped technical and business/stakeholder discovery questions for early/vague tasks. Takes precedence over the legacy boolean design field."
|
|
23494
24871
|
),
|
|
23495
|
-
design:
|
|
24872
|
+
design: z8.boolean().optional().describe(
|
|
23496
24873
|
'Legacy compatibility flag: set to true for web-page/UI design ideation focused on visual appeal and conversion. New callers should use mode: "design" instead. Omit this field when not requesting design mode; absent is treated as false.'
|
|
23497
24874
|
)
|
|
23498
24875
|
}
|
|
@@ -23584,7 +24961,7 @@ registerTool(
|
|
|
23584
24961
|
},
|
|
23585
24962
|
description: "Use to retrieve the result envelope for a previously submitted brainstorm by brainstorm_id. Returns opinion-provider rows only (including error_kind for each row). Does NOT start a new brainstorm \u2014 use request_brainstorm first if none exists. Returns not-found when still processing.",
|
|
23586
24963
|
inputSchema: {
|
|
23587
|
-
brainstorm_id:
|
|
24964
|
+
brainstorm_id: z8.string().describe(
|
|
23588
24965
|
"The brainstorm_id (UUID) returned by request_brainstorm."
|
|
23589
24966
|
),
|
|
23590
24967
|
repo_name: commonFields.repo_name,
|
|
@@ -23627,10 +25004,10 @@ registerTool(
|
|
|
23627
25004
|
},
|
|
23628
25005
|
description: "Create a pull request on the configured VCS provider (GitHub or Bitbucket). Returns a structured response with {available, reason, action, detail}. If a PR already exists for the head branch, returns it with created=false. Capability issues (missing VCS config, API errors) return available=false, not errors. The repo_name is automatically injected from the configured environment.",
|
|
23629
25006
|
inputSchema: {
|
|
23630
|
-
head_branch:
|
|
23631
|
-
base_branch:
|
|
23632
|
-
title:
|
|
23633
|
-
body:
|
|
25007
|
+
head_branch: z8.string().describe("The source branch name for the pull request"),
|
|
25008
|
+
base_branch: z8.string().describe("The target/destination branch name for the pull request"),
|
|
25009
|
+
title: z8.string().describe("The title of the pull request"),
|
|
25010
|
+
body: z8.string().optional().describe("The description/body of the pull request")
|
|
23634
25011
|
}
|
|
23635
25012
|
},
|
|
23636
25013
|
async ({ head_branch, base_branch, title, body }) => {
|
|
@@ -23664,8 +25041,8 @@ var resolveCiChecksTool = registerTool(
|
|
|
23664
25041
|
},
|
|
23665
25042
|
description: "Discover and classify CI checks for the configured repository. Queries GitHub Check Runs + Commit Statuses APIs (or Bitbucket Build Statuses), then uses Branch Protection API or LLM to determine which checks are required for merging. Results are cached per-project \u2014 subsequent calls return cached config unless force_rerun is true. Returns {available, reason, action, detail} envelope. The repo_name is automatically injected from the configured environment.",
|
|
23666
25043
|
inputSchema: {
|
|
23667
|
-
commit_ref:
|
|
23668
|
-
force_rerun:
|
|
25044
|
+
commit_ref: z8.string().describe("Git commit SHA to discover checks for"),
|
|
25045
|
+
force_rerun: z8.boolean().optional().describe("Set to true to bypass cache and re-resolve CI checks")
|
|
23669
25046
|
}
|
|
23670
25047
|
},
|
|
23671
25048
|
async ({ commit_ref, force_rerun }) => {
|
|
@@ -23704,7 +25081,7 @@ var pollCiChecksTool = registerTool(
|
|
|
23704
25081
|
},
|
|
23705
25082
|
description: "Poll the current status of CI checks for a specific commit. Requires that resolve_ci_checks has been called first to populate the check configuration. Returns per-check status, all_complete, all_passed, and unknown_checks fields. For failed checks with detail_level 'full', includes annotations and/or log tails. Returns {available, reason, action, detail} envelope. The repo_name is automatically injected from the configured environment.",
|
|
23706
25083
|
inputSchema: {
|
|
23707
|
-
commit_ref:
|
|
25084
|
+
commit_ref: z8.string().describe("Git commit SHA to poll CI checks for")
|
|
23708
25085
|
}
|
|
23709
25086
|
},
|
|
23710
25087
|
async ({ commit_ref }) => {
|
|
@@ -23801,14 +25178,14 @@ registerTool(
|
|
|
23801
25178
|
},
|
|
23802
25179
|
description: "Retrieve a fully resolved pipeline recipe by name. Substitutes variables, resolves instruction file references to inline content, and returns an ordered array of executable steps. Each step is either an mcp_call (with tool name and params) or an agent_task (with instruction text). Use list_pipelines to discover available pipeline names first. Note: the 'docs_dir' variable is automatically set from BAPI_DOCS_DIR \u2014 callers should omit it.",
|
|
23803
25180
|
inputSchema: {
|
|
23804
|
-
pipeline:
|
|
23805
|
-
variables:
|
|
25181
|
+
pipeline: z8.string().describe("Pipeline name (e.g. 'review-ticket', 'implement-ticket')"),
|
|
25182
|
+
variables: z8.record(z8.string(), z8.string()).optional().describe(
|
|
23806
25183
|
"Key-value pairs for variable substitution (e.g. { ticket_key: 'BAPI-123' })"
|
|
23807
25184
|
),
|
|
23808
|
-
skip_steps:
|
|
25185
|
+
skip_steps: z8.array(z8.string()).optional().describe(
|
|
23809
25186
|
"Step tool names or descriptions to omit from the recipe"
|
|
23810
25187
|
),
|
|
23811
|
-
auto_approve:
|
|
25188
|
+
auto_approve: z8.boolean().optional().describe(
|
|
23812
25189
|
"When true, automatically approve all approval-gated steps. For implement-ticket this skips the commit/push approval pause; for review-ticket this skips the HTML decision page and selects each item's recommended option. Pass via this top-level parameter, not via the variables map."
|
|
23813
25190
|
)
|
|
23814
25191
|
}
|
|
@@ -23880,7 +25257,7 @@ registerTool(
|
|
|
23880
25257
|
}
|
|
23881
25258
|
}
|
|
23882
25259
|
);
|
|
23883
|
-
if (
|
|
25260
|
+
if (ACTIVE_GROUPS.has("pipeline-authoring")) {
|
|
23884
25261
|
registerTool(
|
|
23885
25262
|
"list_pipelines",
|
|
23886
25263
|
{
|
|
@@ -23919,14 +25296,14 @@ if (profileIncludes(BRIDGE_MCP_PROFILE, "pipeline-authoring")) {
|
|
|
23919
25296
|
},
|
|
23920
25297
|
description: "Execute a Bridge API pipeline by name. The orchestrator runs steps sequentially, dispatching mcp_call steps in-process and pausing on agent_task steps with a needs_agent_task envelope. Returns a unified envelope keyed on `status`: `completed` (terminal success with `results`), `needs_agent_task` (pause \u2014 read `instruction`, perform the task, then call `resume_pipeline` with the resulting string as `agent_result`), or `failed` (terminal error \u2014 check `error_code`: VALIDATION | NOT_FOUND | EXPIRED | REPO_MISMATCH | TOOL_ERROR). Paused runs auto-expire after an idle TTL (default 24 hours; override with `ttl_seconds`). The TTL is reset on every state transition.\n\nCall list_pipelines for the current resolved catalog of available pipeline names (bundled plus any custom user pipelines).",
|
|
23921
25298
|
inputSchema: {
|
|
23922
|
-
pipeline:
|
|
23923
|
-
variables:
|
|
25299
|
+
pipeline: z8.string().describe("Pipeline name (e.g. 'review-ticket', 'implement-ticket')"),
|
|
25300
|
+
variables: z8.record(z8.string(), z8.string()).optional().describe(
|
|
23924
25301
|
"Key-value pairs for variable substitution (e.g. { ticket_key: 'BAPI-123' }). Do NOT pass `auto_approve` here \u2014 use the top-level parameter."
|
|
23925
25302
|
),
|
|
23926
|
-
auto_approve:
|
|
25303
|
+
auto_approve: z8.union([z8.boolean(), z8.literal("true"), z8.literal("false")]).optional().describe(
|
|
23927
25304
|
"When true, approval-gated mcp_call steps execute directly. When false or omitted, the orchestrator synthesises a needs_agent_task pause so the agent can confirm with the user before resuming. Accepts boolean or 'true'/'false' strings for MCP clients that serialize booleans as strings."
|
|
23928
25305
|
),
|
|
23929
|
-
ttl_seconds:
|
|
25306
|
+
ttl_seconds: z8.number().int().positive().optional().describe(
|
|
23930
25307
|
"Override the default 24-hour idle TTL for this run. Must be a positive integer."
|
|
23931
25308
|
)
|
|
23932
25309
|
}
|
|
@@ -23954,8 +25331,8 @@ if (profileIncludes(BRIDGE_MCP_PROFILE, "pipeline-authoring")) {
|
|
|
23954
25331
|
},
|
|
23955
25332
|
description: "Resume a paused pipeline run with the result of the agent_task. Provide the `pipeline_run_id` returned by the prior needs_agent_task envelope, and the string the instruction's `## Return` section asked you to produce as `agent_result`. `agent_result` is always a string \u2014 do not wrap it in JSON unless the instruction explicitly asked you to serialize structured output. Returns the same unified envelope shape as `run_pipeline`.",
|
|
23956
25333
|
inputSchema: {
|
|
23957
|
-
pipeline_run_id:
|
|
23958
|
-
agent_result:
|
|
25334
|
+
pipeline_run_id: z8.string().describe("The pipeline_run_id returned by a prior needs_agent_task envelope"),
|
|
25335
|
+
agent_result: z8.string().describe(
|
|
23959
25336
|
"The string the paused instruction's ## Return section asked you to produce"
|
|
23960
25337
|
)
|
|
23961
25338
|
}
|
|
@@ -23983,7 +25360,7 @@ if (profileIncludes(BRIDGE_MCP_PROFILE, "pipeline-authoring")) {
|
|
|
23983
25360
|
},
|
|
23984
25361
|
description: "List recent pipeline runs for the configured repository, newest first. Returns metadata only \u2014 `resolved_recipe`, resolved params, instruction text, results, and agent outputs are intentionally excluded. Use this to recover a `pipeline_run_id` when an earlier needs_agent_task envelope is no longer in scope (e.g. after compaction or a client restart). Optionally filter by `status`: running | paused | completed | failed | expired.",
|
|
23985
25362
|
inputSchema: {
|
|
23986
|
-
status:
|
|
25363
|
+
status: z8.enum(["running", "paused", "completed", "failed", "expired"]).optional().describe("Optional status filter")
|
|
23987
25364
|
}
|
|
23988
25365
|
},
|
|
23989
25366
|
async (input) => {
|
|
@@ -24009,7 +25386,7 @@ if (profileIncludes(BRIDGE_MCP_PROFILE, "pipeline-authoring")) {
|
|
|
24009
25386
|
},
|
|
24010
25387
|
description: "Delete a pipeline run row (any status). Use this to discard orphaned `running` rows from a previous session that can't be resumed (resume_pipeline only accepts `paused`), to clean up after a failed run, or to remove a no-longer-needed paused session. Returns `{ status: 'completed', deleted: true, pipeline_run_id }` on success, or a `failed` envelope with error_code in (VALIDATION | NOT_FOUND | REPO_MISMATCH | TOOL_ERROR). Repo-scoped: the row's stored repo_name must match the caller's repo.",
|
|
24011
25388
|
inputSchema: {
|
|
24012
|
-
pipeline_run_id:
|
|
25389
|
+
pipeline_run_id: z8.string().describe("UUID of the pipeline run to delete.")
|
|
24013
25390
|
}
|
|
24014
25391
|
},
|
|
24015
25392
|
async (input) => {
|
|
@@ -24036,14 +25413,14 @@ registerTool(
|
|
|
24036
25413
|
},
|
|
24037
25414
|
description: "Run the full-automation chain for an idea: create ticket(s) (idea-to-ticket), review each created ticket (review-ticket fan-out), then emit the exact `/start-tickets ...` command for you to invoke in this same session. Returns the chain envelope keyed on `status`: `needs_agent_task` (perform the `next_action.instruction`, then call `resume_full_automation` with the result as `agent_result`), `completed`, or `failed` (check `error_code`: VALIDATION | NOT_FOUND | EXPIRED | REPO_MISMATCH | TOOL_ERROR). Provide the idea via `idea` or `idea_file` (mutually exclusive).",
|
|
24038
25415
|
inputSchema: {
|
|
24039
|
-
idea:
|
|
24040
|
-
idea_file:
|
|
24041
|
-
auto_approve:
|
|
24042
|
-
scheduled_at:
|
|
24043
|
-
max_children:
|
|
24044
|
-
allow_duplicate:
|
|
24045
|
-
agent:
|
|
24046
|
-
ttl_seconds:
|
|
25416
|
+
idea: z8.string().optional(),
|
|
25417
|
+
idea_file: z8.string().optional(),
|
|
25418
|
+
auto_approve: z8.union([z8.boolean(), z8.literal("true"), z8.literal("false")]).optional(),
|
|
25419
|
+
scheduled_at: z8.string().optional(),
|
|
25420
|
+
max_children: z8.number().int().positive().optional(),
|
|
25421
|
+
allow_duplicate: z8.boolean().optional(),
|
|
25422
|
+
agent: z8.enum(["claude"]).optional(),
|
|
25423
|
+
ttl_seconds: z8.number().int().positive().optional()
|
|
24047
25424
|
}
|
|
24048
25425
|
},
|
|
24049
25426
|
async (input) => {
|
|
@@ -24086,8 +25463,8 @@ registerTool(
|
|
|
24086
25463
|
},
|
|
24087
25464
|
description: "Resume a paused full-automation chain run. Provide the `chain_run_id` returned by the prior needs_agent_task envelope and the string the instruction asked you to produce as `agent_result`. Returns the same chain envelope shape as `run_full_automation`.",
|
|
24088
25465
|
inputSchema: {
|
|
24089
|
-
chain_run_id:
|
|
24090
|
-
agent_result:
|
|
25466
|
+
chain_run_id: z8.string(),
|
|
25467
|
+
agent_result: z8.string()
|
|
24091
25468
|
}
|
|
24092
25469
|
},
|
|
24093
25470
|
async (input) => {
|
|
@@ -24106,7 +25483,7 @@ function containsUnsafeEncodedPathToken(value) {
|
|
|
24106
25483
|
return /%2e/i.test(value) || /%2f/i.test(value) || /%5c/i.test(value);
|
|
24107
25484
|
}
|
|
24108
25485
|
function isPlatformAbsolutePath(value) {
|
|
24109
|
-
return
|
|
25486
|
+
return path26.posix.isAbsolute(value) || path26.win32.isAbsolute(value) || path26.isAbsolute(value);
|
|
24110
25487
|
}
|
|
24111
25488
|
function validateDecisionPageOutputSubdir(value) {
|
|
24112
25489
|
if (value.trim().length === 0) {
|
|
@@ -24156,9 +25533,9 @@ async function resolveDecisionPageOutputTarget(outputSubdir, outputFilename) {
|
|
|
24156
25533
|
if (subdirError) return { ok: false, message: subdirError };
|
|
24157
25534
|
const filenameError = validateDecisionPageOutputFilename(outputFilename);
|
|
24158
25535
|
if (filenameError) return { ok: false, message: filenameError };
|
|
24159
|
-
const docsBase =
|
|
24160
|
-
const resolvedTarget =
|
|
24161
|
-
if (!resolvedTarget.startsWith(docsBase +
|
|
25536
|
+
const docsBase = path26.resolve(await getDocsDir());
|
|
25537
|
+
const resolvedTarget = path26.resolve(docsBase, outputSubdir, outputFilename);
|
|
25538
|
+
if (!resolvedTarget.startsWith(docsBase + path26.sep)) {
|
|
24162
25539
|
return {
|
|
24163
25540
|
ok: false,
|
|
24164
25541
|
message: `Invalid output target: the resolved output path must stay under the docs directory.`
|
|
@@ -24166,7 +25543,7 @@ async function resolveDecisionPageOutputTarget(outputSubdir, outputFilename) {
|
|
|
24166
25543
|
}
|
|
24167
25544
|
return {
|
|
24168
25545
|
ok: true,
|
|
24169
|
-
docsPath:
|
|
25546
|
+
docsPath: path26.dirname(resolvedTarget),
|
|
24170
25547
|
filePath: resolvedTarget
|
|
24171
25548
|
};
|
|
24172
25549
|
}
|
|
@@ -24211,7 +25588,7 @@ registerTool(
|
|
|
24211
25588
|
try {
|
|
24212
25589
|
parsed = DecisionPageInputSchema.parse(rawPayload);
|
|
24213
25590
|
} catch (err) {
|
|
24214
|
-
if (err instanceof
|
|
25591
|
+
if (err instanceof z8.ZodError) {
|
|
24215
25592
|
return validationError(formatDecisionPageValidationError(err));
|
|
24216
25593
|
}
|
|
24217
25594
|
throw err;
|
|
@@ -24254,43 +25631,43 @@ registerTool(
|
|
|
24254
25631
|
return validationError(outputTarget.message);
|
|
24255
25632
|
}
|
|
24256
25633
|
const projectRootForAssets = await getProjectRoot();
|
|
24257
|
-
const pkgRoot =
|
|
25634
|
+
const pkgRoot = path26.resolve(path26.dirname(fileURLToPath4(import.meta.url)), "../");
|
|
24258
25635
|
let assetsDir;
|
|
24259
25636
|
try {
|
|
24260
|
-
await stat8(
|
|
24261
|
-
assetsDir =
|
|
25637
|
+
await stat8(path26.join(projectRootForAssets, "design-assets"));
|
|
25638
|
+
assetsDir = path26.join(projectRootForAssets, "design-assets");
|
|
24262
25639
|
} catch {
|
|
24263
|
-
assetsDir =
|
|
25640
|
+
assetsDir = path26.join(pkgRoot, "design-assets");
|
|
24264
25641
|
}
|
|
24265
25642
|
let fontsDir;
|
|
24266
25643
|
try {
|
|
24267
|
-
await stat8(
|
|
24268
|
-
fontsDir =
|
|
25644
|
+
await stat8(path26.join(projectRootForAssets, "public", "fonts"));
|
|
25645
|
+
fontsDir = path26.join(projectRootForAssets, "public", "fonts");
|
|
24269
25646
|
} catch {
|
|
24270
|
-
fontsDir =
|
|
25647
|
+
fontsDir = path26.join(pkgRoot, "public", "fonts");
|
|
24271
25648
|
}
|
|
24272
25649
|
let faviconBase64 = "";
|
|
24273
25650
|
let logoBase64 = "";
|
|
24274
25651
|
try {
|
|
24275
|
-
const faviconBuf = await
|
|
25652
|
+
const faviconBuf = await readFile11(path26.join(assetsDir, "favicon", "favicon-32x32.png"));
|
|
24276
25653
|
faviconBase64 = faviconBuf.toString("base64");
|
|
24277
25654
|
} catch {
|
|
24278
25655
|
}
|
|
24279
25656
|
try {
|
|
24280
|
-
const logoBuf = await
|
|
25657
|
+
const logoBuf = await readFile11(path26.join(assetsDir, "just-logo-rough-draft.png"));
|
|
24281
25658
|
logoBase64 = logoBuf.toString("base64");
|
|
24282
25659
|
} catch {
|
|
24283
25660
|
}
|
|
24284
25661
|
const docsPath = outputTarget.docsPath;
|
|
24285
25662
|
const filePath = outputTarget.filePath;
|
|
24286
|
-
const fontsRelPath =
|
|
25663
|
+
const fontsRelPath = path26.relative(docsPath, fontsDir);
|
|
24287
25664
|
const html = generateDecisionPageHtml(parsed, {
|
|
24288
25665
|
faviconBase64,
|
|
24289
25666
|
logoBase64,
|
|
24290
25667
|
fontsRelPath
|
|
24291
25668
|
});
|
|
24292
|
-
await
|
|
24293
|
-
await
|
|
25669
|
+
await mkdir10(docsPath, { recursive: true });
|
|
25670
|
+
await writeFile10(filePath, html, "utf-8");
|
|
24294
25671
|
return {
|
|
24295
25672
|
content: [{
|
|
24296
25673
|
type: "text",
|