@bridge_gpt/mcp-server 0.2.24 → 0.2.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +98 -28
- package/build/agents.generated.js +1 -1
- package/build/bridge-api-urls.js +31 -0
- package/build/commands.generated.js +5 -5
- package/build/conductor/epic-reconcile.js +7 -1
- package/build/conductor/epic-runtime.js +5 -0
- package/build/conductor-bundle-artifacts.js +802 -0
- package/build/conductor-bundle-cli.js +256 -0
- package/build/connect-github-api.js +365 -0
- package/build/connect-github.js +415 -0
- package/build/decision-page-schema.js +34 -5
- package/build/decision-page-template.js +117 -35
- package/build/docs.generated.js +2 -1
- package/build/doctor.js +148 -1
- package/build/env-flags.js +31 -0
- package/build/index.js +3467 -498
- package/build/init.js +7 -3
- package/build/install-bridge.js +624 -38
- package/build/install-doctor.js +64 -0
- package/build/mcp-host-config.js +521 -0
- package/build/mcp-host-targets.js +194 -0
- package/build/mcp-install-state.js +175 -0
- package/build/pipelines.generated.js +127 -132
- package/build/readme.generated.js +1 -1
- package/build/start-tickets.js +166 -18
- package/build/tool-surface-gating.js +396 -0
- package/build/version.generated.js +1 -1
- package/docs/install/github-app.md +80 -17
- package/docs/install/mcp-tool-integrations.md +2 -2
- package/package.json +5 -5
- package/pipelines/learn-repository.json +111 -119
- package/public/css/main.min.css +258 -65
- package/public/css/main.min.css.map +1 -1
- package/public/js/main.min.js +188 -92
- package/public/js/main.min.js.map +1 -1
- package/smoke-test/SMOKE-TEST.md +4 -4
|
@@ -64,7 +64,11 @@ export function generateDecisionPageHtml(data, assets = DEFAULT_ASSETS) {
|
|
|
64
64
|
const { ticket_key, actionable_items, clear_improvements } = data;
|
|
65
65
|
const hasDecisions = actionable_items.length > 0;
|
|
66
66
|
const isPlanning = data.artifact_type === "pre_ticket_planning";
|
|
67
|
-
|
|
67
|
+
// Acceptance criteria and NFRs both render stance controls, and the whole
|
|
68
|
+
// <script> block is gated on needsForm — so a page carrying either one must set
|
|
69
|
+
// it, or the radios render with no listeners and no capture loop behind them.
|
|
70
|
+
const stanceItemCount = (data.system_goals?.nfrs?.length ?? 0) + (data.system_goals?.acceptance_criteria?.length ?? 0);
|
|
71
|
+
const needsForm = hasDecisions || (isPlanning && stanceItemCount > 0);
|
|
68
72
|
const effectiveLabels = resolveDecisionPageLabels(data.labels);
|
|
69
73
|
const faviconLink = assets.faviconBase64
|
|
70
74
|
? `<link rel="icon" type="image/png" sizes="32x32" href="data:image/png;base64,${assets.faviconBase64}">`
|
|
@@ -437,18 +441,18 @@ ${fontFaces}
|
|
|
437
441
|
color: var(--text-color);
|
|
438
442
|
white-space: pre-wrap;
|
|
439
443
|
}
|
|
440
|
-
.nfr-list, .order-list {
|
|
444
|
+
.nfr-list, .ac-list, .order-list {
|
|
441
445
|
list-style: none;
|
|
442
446
|
padding: 0;
|
|
443
447
|
margin-top: 0.5rem;
|
|
444
448
|
}
|
|
445
|
-
.nfr-list li, .order-list li {
|
|
449
|
+
.nfr-list li, .ac-list li, .order-list li {
|
|
446
450
|
padding: 0.75rem 0;
|
|
447
451
|
border-bottom: 1px solid var(--border-color);
|
|
448
452
|
}
|
|
449
|
-
.nfr-list li:last-child, .order-list li:last-child { border-bottom: none; }
|
|
450
|
-
.nfr-category { font-weight: 600; }
|
|
451
|
-
.nfr-implication, .order-meta {
|
|
453
|
+
.nfr-list li:last-child, .ac-list li:last-child, .order-list li:last-child { border-bottom: none; }
|
|
454
|
+
.nfr-category, .ac-id { font-weight: 600; }
|
|
455
|
+
.nfr-implication, .ac-verification, .order-meta {
|
|
452
456
|
font-size: 0.9rem;
|
|
453
457
|
color: var(--secondary-color);
|
|
454
458
|
margin-top: 0.25rem;
|
|
@@ -481,20 +485,22 @@ ${fontFaces}
|
|
|
481
485
|
border-color: var(--primary-color);
|
|
482
486
|
box-shadow: 0 0 0 3px rgba(226, 98, 75, 0.2);
|
|
483
487
|
}
|
|
484
|
-
|
|
488
|
+
/* display:block !important deliberately defeats the generic .hidden rule so
|
|
489
|
+
the max-height transition below can run; .hidden then collapses it. */
|
|
490
|
+
.nfr-feedback .comment-area, .ac-feedback .comment-area {
|
|
485
491
|
display: block !important;
|
|
486
492
|
overflow: hidden;
|
|
487
493
|
transition: max-height 150ms cubic-bezier(0.4, 0, 0.2, 1),
|
|
488
494
|
opacity 150ms cubic-bezier(0.4, 0, 0.2, 1),
|
|
489
495
|
margin-top 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
490
496
|
}
|
|
491
|
-
.nfr-feedback .comment-area.hidden {
|
|
497
|
+
.nfr-feedback .comment-area.hidden, .ac-feedback .comment-area.hidden {
|
|
492
498
|
max-height: 0;
|
|
493
499
|
opacity: 0;
|
|
494
500
|
margin-top: 0;
|
|
495
501
|
pointer-events: none;
|
|
496
502
|
}
|
|
497
|
-
.nfr-feedback .radio-group {
|
|
503
|
+
.nfr-feedback .radio-group, .ac-feedback .radio-group {
|
|
498
504
|
display: flex;
|
|
499
505
|
flex-direction: row;
|
|
500
506
|
gap: 1.5rem;
|
|
@@ -508,7 +514,7 @@ ${fontFaces}
|
|
|
508
514
|
align-items: stretch;
|
|
509
515
|
flex-direction: column;
|
|
510
516
|
}
|
|
511
|
-
.nfr-feedback .radio-group {
|
|
517
|
+
.nfr-feedback .radio-group, .ac-feedback .radio-group {
|
|
512
518
|
flex-direction: column;
|
|
513
519
|
gap: 0.75rem;
|
|
514
520
|
}
|
|
@@ -583,14 +589,81 @@ function renderNoDecisions(isPlanning = false) {
|
|
|
583
589
|
<p>${escapeHtml(message)}</p>
|
|
584
590
|
</div>`;
|
|
585
591
|
}
|
|
586
|
-
//
|
|
587
|
-
//
|
|
588
|
-
//
|
|
589
|
-
// When isPlanning is true
|
|
590
|
-
//
|
|
592
|
+
// Renders the acceptance-criteria list — what the system must do — as the
|
|
593
|
+
// centerpiece of the planning panel, above the NFRs. Returns "" when the list is
|
|
594
|
+
// empty so pages that predate acceptance criteria (plan-epic) are unchanged.
|
|
595
|
+
// When isPlanning is true each criterion carries the same Agreed / Ask about this
|
|
596
|
+
// / Disagree stance control the NFR list uses.
|
|
597
|
+
function renderAcceptanceCriteriaList(acs, isPlanning) {
|
|
598
|
+
if (acs.length === 0)
|
|
599
|
+
return "";
|
|
600
|
+
let items = "";
|
|
601
|
+
for (let i = 0; i < acs.length; i++) {
|
|
602
|
+
const ac = acs[i];
|
|
603
|
+
const statusClass = ac.status === "confirmed"
|
|
604
|
+
? "status-confirmed"
|
|
605
|
+
: ac.status === "assumed"
|
|
606
|
+
? "status-assumed"
|
|
607
|
+
: "status-open";
|
|
608
|
+
// acId is used as data-ac-id (becomes the JSON output key) and aria-label.
|
|
609
|
+
// acHtmlId is a whitespace-free variant for id/name/for attributes — HTML5
|
|
610
|
+
// forbids spaces in id values. The loop index i prefixes the radio/textarea
|
|
611
|
+
// identifiers so each criterion is its own radio group, mirroring the NFR
|
|
612
|
+
// list's disambiguation: without it, two ids that collapse to the same slug
|
|
613
|
+
// would share a name and clobber each other's selection.
|
|
614
|
+
const acId = escapeHtml(ac.id);
|
|
615
|
+
const acHtmlId = escapeHtml(ac.id.replace(/\s+/g, "-"));
|
|
616
|
+
const radioName = `ac-stance-${i}-${acHtmlId}`;
|
|
617
|
+
const textareaId = `ac-comment-${i}-${acHtmlId}`;
|
|
618
|
+
const stanceControls = isPlanning ? `
|
|
619
|
+
<div class="ac-feedback" data-ac-id="${acId}">
|
|
620
|
+
<div class="radio-group" role="radiogroup" aria-label="Stance on ${acId}">
|
|
621
|
+
<div class="radio-option">
|
|
622
|
+
<input type="radio" id="${radioName}-agreed" name="${radioName}" value="agreed" checked data-testid="ac-stance-radio">
|
|
623
|
+
<label for="${radioName}-agreed">Agreed</label>
|
|
624
|
+
</div>
|
|
625
|
+
<div class="radio-option">
|
|
626
|
+
<input type="radio" id="${radioName}-ask" name="${radioName}" value="ask" data-testid="ac-stance-radio">
|
|
627
|
+
<label for="${radioName}-ask">Ask about this</label>
|
|
628
|
+
</div>
|
|
629
|
+
<div class="radio-option">
|
|
630
|
+
<input type="radio" id="${radioName}-disagree" name="${radioName}" value="disagree" data-testid="ac-stance-radio">
|
|
631
|
+
<label for="${radioName}-disagree">Disagree</label>
|
|
632
|
+
</div>
|
|
633
|
+
</div>
|
|
634
|
+
<div class="comment-area hidden">
|
|
635
|
+
<label for="${textareaId}">Comment</label>
|
|
636
|
+
<textarea id="${textareaId}" name="${textareaId}" placeholder="Explain your question or concern..." data-testid="ac-comment"></textarea>
|
|
637
|
+
</div>
|
|
638
|
+
</div>` : "";
|
|
639
|
+
items += `
|
|
640
|
+
<li data-testid="system-goal-ac" data-status="${escapeHtml(ac.status)}">
|
|
641
|
+
<span class="ac-id">${escapeHtml(ac.id)}</span><span class="status-tag ${statusClass}">${escapeHtml(ac.status)}</span>
|
|
642
|
+
<div class="goal-body">${escapeHtml(ac.criterion)}</div>
|
|
643
|
+
<div class="ac-verification">Verify: ${escapeHtml(ac.verification)}</div>${stanceControls}
|
|
644
|
+
</li>`;
|
|
645
|
+
}
|
|
646
|
+
return `
|
|
647
|
+
<div class="goal-label">Acceptance criteria</div>
|
|
648
|
+
<ul class="ac-list">${items}
|
|
649
|
+
</ul>`;
|
|
650
|
+
}
|
|
651
|
+
// System-goals panel for the pre_ticket_planning artifact. Returns "" when no
|
|
652
|
+
// goals are supplied so the review_decisions page is byte-for-byte unchanged.
|
|
653
|
+
// Every model-supplied string is escaped via escapeHtml().
|
|
654
|
+
// When isPlanning is true, each acceptance criterion and each NFR renders an
|
|
655
|
+
// interactive stance control (Agreed / Ask about this / Disagree) with a
|
|
656
|
+
// conditional comment textarea.
|
|
591
657
|
function renderSystemGoals(goals, isPlanning = false) {
|
|
592
658
|
if (!goals)
|
|
593
659
|
return "";
|
|
660
|
+
const acceptanceCriteria = goals.acceptance_criteria ?? [];
|
|
661
|
+
const acHtml = renderAcceptanceCriteriaList(acceptanceCriteria, isPlanning);
|
|
662
|
+
// The heading leads with acceptance criteria only when there are some. Pages
|
|
663
|
+
// that carry goals + NFRs alone (plan-epic) keep the original heading.
|
|
664
|
+
const sectionHeading = acceptanceCriteria.length > 0
|
|
665
|
+
? "Acceptance Criteria & System Goals"
|
|
666
|
+
: "System Goals & Non-Functional Requirements";
|
|
594
667
|
const nfrs = goals.nfrs ?? [];
|
|
595
668
|
let nfrHtml = "";
|
|
596
669
|
if (nfrs.length > 0) {
|
|
@@ -647,7 +720,7 @@ function renderSystemGoals(goals, isPlanning = false) {
|
|
|
647
720
|
</ul>`;
|
|
648
721
|
}
|
|
649
722
|
return ` <section class="planning-section" data-testid="system-goals">
|
|
650
|
-
<h2
|
|
723
|
+
<h2>${sectionHeading}</h2>
|
|
651
724
|
<div class="goal-row">
|
|
652
725
|
<div class="goal-label">Business goal</div>
|
|
653
726
|
<div class="goal-body" data-testid="system-goal-business">${escapeHtml(goals.business_goal)}</div>
|
|
@@ -659,7 +732,7 @@ function renderSystemGoals(goals, isPlanning = false) {
|
|
|
659
732
|
<div class="goal-row">
|
|
660
733
|
<div class="goal-label">System behavior</div>
|
|
661
734
|
<div class="goal-body" data-testid="system-goal-behavior">${escapeHtml(goals.system_behavior)}</div>
|
|
662
|
-
</div>${nfrHtml}
|
|
735
|
+
</div>${acHtml}${nfrHtml}
|
|
663
736
|
</section>`;
|
|
664
737
|
}
|
|
665
738
|
// Read-only recommended implementation order (epic surfaces). Returns "" when the
|
|
@@ -853,9 +926,11 @@ function renderScript(data, isPlanning = false) {
|
|
|
853
926
|
postSubmitContainer = document.getElementById("post-submit-container");
|
|
854
927
|
jsonOutput = document.getElementById("json-output");
|
|
855
928
|
|
|
856
|
-
${isPlanning ? `//
|
|
857
|
-
|
|
858
|
-
|
|
929
|
+
${isPlanning ? `// Stance radio disclosure (planning mode only). Acceptance criteria and
|
|
930
|
+
// NFRs behave identically here, so they share one listener set; only the
|
|
931
|
+
// capture step below distinguishes them, since their JSON keys differ.
|
|
932
|
+
var stanceContainers = document.querySelectorAll(".nfr-feedback, .ac-feedback");
|
|
933
|
+
stanceContainers.forEach(function(container) {
|
|
859
934
|
var radios = container.querySelectorAll('input[type="radio"]');
|
|
860
935
|
radios.forEach(function(radio) {
|
|
861
936
|
radio.addEventListener("change", function() {
|
|
@@ -874,7 +949,7 @@ function renderScript(data, isPlanning = false) {
|
|
|
874
949
|
}
|
|
875
950
|
});
|
|
876
951
|
});
|
|
877
|
-
});` : `var
|
|
952
|
+
});` : `var stanceContainers = [];`}
|
|
878
953
|
|
|
879
954
|
submitBtn.addEventListener("click", function() {
|
|
880
955
|
var cards = document.querySelectorAll(".card[data-item-id]");
|
|
@@ -909,8 +984,8 @@ function renderScript(data, isPlanning = false) {
|
|
|
909
984
|
}
|
|
910
985
|
});
|
|
911
986
|
|
|
912
|
-
// Validate
|
|
913
|
-
|
|
987
|
+
// Validate stance feedback: ask/disagree requires a comment.
|
|
988
|
+
stanceContainers.forEach(function(container) {
|
|
914
989
|
var selected = container.querySelector('input[type="radio"]:checked');
|
|
915
990
|
if (!selected) return;
|
|
916
991
|
if (selected.value === "ask" || selected.value === "disagree") {
|
|
@@ -957,22 +1032,29 @@ function renderScript(data, isPlanning = false) {
|
|
|
957
1032
|
};
|
|
958
1033
|
});
|
|
959
1034
|
|
|
960
|
-
${isPlanning ? `// Capture
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
var
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
1035
|
+
${isPlanning ? `// Capture stances. Acceptance criteria and NFRs land in separate maps
|
|
1036
|
+
// because their keys differ: AC are keyed by id, NFRs by category.
|
|
1037
|
+
function captureStances(selector, idAttr) {
|
|
1038
|
+
var out = {};
|
|
1039
|
+
document.querySelectorAll(selector).forEach(function(container) {
|
|
1040
|
+
var key = container.getAttribute(idAttr);
|
|
1041
|
+
var selected = container.querySelector('input[type="radio"]:checked');
|
|
1042
|
+
var commentArea = container.querySelector(".comment-area");
|
|
1043
|
+
var textarea = commentArea ? commentArea.querySelector("textarea") : null;
|
|
1044
|
+
out[key] = {
|
|
1045
|
+
stance: selected ? selected.value : "agreed",
|
|
1046
|
+
comment: textarea ? textarea.value : ""
|
|
1047
|
+
};
|
|
1048
|
+
});
|
|
1049
|
+
return out;
|
|
1050
|
+
}
|
|
1051
|
+
var acceptanceCriteriaFeedback = captureStances(".ac-feedback", "data-ac-id");
|
|
1052
|
+
var nfrFeedback = captureStances(".nfr-feedback", "data-nfr-id");` : ""}
|
|
972
1053
|
|
|
973
1054
|
var output = {
|
|
974
1055
|
ticket_key: ${safeJsonForScript(data.ticket_key)},
|
|
975
1056
|
decisions: decisions,
|
|
1057
|
+
${isPlanning ? "acceptance_criteria_feedback: acceptanceCriteriaFeedback," : ""}
|
|
976
1058
|
${isPlanning ? "nfr_feedback: nfrFeedback," : ""}
|
|
977
1059
|
general_comment: document.getElementById("general-comment").value
|
|
978
1060
|
};
|
package/build/docs.generated.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// AUTO-GENERATED — do not edit manually. Regenerate with: npm run build
|
|
2
2
|
// This file is produced by scripts/bundle-docs.js
|
|
3
3
|
export const DOCS = {
|
|
4
|
-
"docs/mcp-tool-integrations.md": "# MCP tool integrations — the human \"why\" behind the capability report\n\nThis catalog is **explanatory prose only**. It exists so the `/install-bridge`\ncapability report can cite a human-readable \"why\" for each gate. It is **not** a\nsource of truth for gating: the server computes every `locked_tools` /\n`unlocked_tools` membership decision itself and the agent must never recompute a\ntool's dependencies from this document.\n\n**Authoritative source of gating.** The enforced rules — which tools are blocked,\nwhich are degraded, and what each requires — live in\n`api/library/vcs/vcs_route_operations.py`:\n\n- `VCS_ROUTE_REQUIREMENTS` — routes that **BLOCK** (are unavailable) without a\n VCS connection.\n- `VCS_ROUTE_WARNINGS` — routes that **DEGRADE** (stay usable, but without\n codebase context) without a VCS connection.\n- `NEVER_GATED_ROUTE_KEYS` — routes that are never gated on any integration.\n- `INDEX_REQUIRED_ROUTE_KEYS`, `INDEX_REQUIRED_BRAINSTORM_MODES`,\n `CREATE_DOC_CODEBASE_CONTEXT_DOC_TYPES`, `CREATE_DOC_WARN_DOC_TYPES` — the\n conditional \"requires a successful code index\" dimension.\n- The resolver helpers `get_required_vcs_operation()`, `get_warn_vcs_operation()`,\n and `requires_successful_index()` are the authoritative functions that decide a\n case. The capability report is derived from these; this catalog explains them.\n\n## Reading the capability report\n\nEach tool entry the server returns has the exact shape\n`{tool, effect, missing, semantics}`:\n\n- **`effect`**\n - **`BLOCK`** — the tool is **unavailable** until every listed dependency is\n met. It will refuse to run without them.\n - **`DEGRADE`** — the tool is **usable right now**, but **without codebase\n context** (it cannot ground its output in your repository). Connecting the\n listed dependency upgrades it from \"works blind\" to \"works with full context\".\n A `DEGRADE` tool is never \"failed\".\n- **`missing`** — the server-computed dependency identifiers still needed:\n integration ids such as `github_app` / `vcs_access_token`, and the synthetic\n `code_index` (a successful repository index).\n- **`semantics`**\n - **`all_of`** — every id in `missing` is required.\n - **`any_of`** — the VCS-provider candidates in `missing` are alternatives:\n **either** `github_app` **or** `vcs_access_token` satisfies the VCS\n requirement (this is the \"provider unknown\" case). When `code_index` also\n appears, it remains separately required — `semantics` describes only the VCS\n provider candidates, and a code index is always mandatory in addition.\n\nThe three readiness dimensions `configured` / `learned` / `indexed` are reported\nindependently. `indexed` may be `true`, `false`, or `null` — a `null` means the\nindex status could not be confirmed and must **not** be read as \"indexed\".\n\n## The integrations\n\n| Integration id | What it is | What it unlocks |\n| --- | --- | --- |\n| `jira` | Jira API access | Ticket reads/writes, estimation and review automations, status transitions. |\n| `github_app` | GitHub App installation | Pull requests, code review, and private-repo parsing on GitHub projects. |\n| `vcs_access_token` | VCS access token | Pull requests, code review, and private-repo parsing on Bitbucket projects. |\n| `vcs_webhook` | VCS webhook secret | Merge webhooks and CI follow-up triggers. |\n| `code_index` | A successful repository index | Codebase-grounded planning, architecture, reimplementation, and technical/discovery brainstorms. Produced by `/parse-repository`. |\n\nA project's `github_app` **or** `vcs_access_token` provides the VCS connection;\nwhich one applies depends on the project's version-control system. When the\nproject's provider is unknown, either credential satisfies the requirement — the\nreport expresses that as `semantics: any_of`.\n\n## The gates, by capability\n\n### Pull requests and CI (BLOCK on VCS)\n\nTools like `create_pull_request`, `resolve_ci_checks`, `poll_ci_checks`, and\n`materialize_fresh_base` are **unavailable** (`BLOCK`) until a VCS connection is\nconfigured. They act directly on the version-control host, so without a\nconnection there is nothing for them to talk to.\n\n### Repository indexing and maps (BLOCK on VCS)\n\n`parse_repository` and `regenerate_directory_map` need a VCS connection to read\nthe repository. They **BLOCK** until VCS is connected.\n\n### Codebase-grounded generation (BLOCK on VCS **and** a code index)\n\nPlanning and architecture tools — `generate_plan_direct`,\n`generate_architecture_direct`, `request_reimplement_context`,\n`code_writer_generate_plan`, `code_writer_generate_architecture`, and\n`create_doc` for **TDD** / **architecture** documents — ground their output in\nyour indexed codebase. They **BLOCK** until BOTH a VCS connection AND a\nsuccessful code index exist (`all_of`, with `code_index` in `missing`).\n\n###
|
|
4
|
+
"docs/mcp-tool-integrations.md": "# MCP tool integrations — the human \"why\" behind the capability report\n\nThis catalog is **explanatory prose only**. It exists so the `/install-bridge`\ncapability report can cite a human-readable \"why\" for each gate. It is **not** a\nsource of truth for gating: the server computes every `locked_tools` /\n`unlocked_tools` membership decision itself and the agent must never recompute a\ntool's dependencies from this document.\n\n**Authoritative source of gating.** The enforced rules — which tools are blocked,\nwhich are degraded, and what each requires — live in\n`api/library/vcs/vcs_route_operations.py`:\n\n- `VCS_ROUTE_REQUIREMENTS` — routes that **BLOCK** (are unavailable) without a\n VCS connection.\n- `VCS_ROUTE_WARNINGS` — routes that **DEGRADE** (stay usable, but without\n codebase context) without a VCS connection.\n- `NEVER_GATED_ROUTE_KEYS` — routes that are never gated on any integration.\n- `INDEX_REQUIRED_ROUTE_KEYS`, `INDEX_REQUIRED_BRAINSTORM_MODES`,\n `CREATE_DOC_CODEBASE_CONTEXT_DOC_TYPES`, `CREATE_DOC_WARN_DOC_TYPES` — the\n conditional \"requires a successful code index\" dimension.\n- The resolver helpers `get_required_vcs_operation()`, `get_warn_vcs_operation()`,\n and `requires_successful_index()` are the authoritative functions that decide a\n case. The capability report is derived from these; this catalog explains them.\n\n## Reading the capability report\n\nEach tool entry the server returns has the exact shape\n`{tool, effect, missing, semantics}`:\n\n- **`effect`**\n - **`BLOCK`** — the tool is **unavailable** until every listed dependency is\n met. It will refuse to run without them.\n - **`DEGRADE`** — the tool is **usable right now**, but **without codebase\n context** (it cannot ground its output in your repository). Connecting the\n listed dependency upgrades it from \"works blind\" to \"works with full context\".\n A `DEGRADE` tool is never \"failed\".\n- **`missing`** — the server-computed dependency identifiers still needed:\n integration ids such as `github_app` / `vcs_access_token`, and the synthetic\n `code_index` (a successful repository index).\n- **`semantics`**\n - **`all_of`** — every id in `missing` is required.\n - **`any_of`** — the VCS-provider candidates in `missing` are alternatives:\n **either** `github_app` **or** `vcs_access_token` satisfies the VCS\n requirement (this is the \"provider unknown\" case). When `code_index` also\n appears, it remains separately required — `semantics` describes only the VCS\n provider candidates, and a code index is always mandatory in addition.\n\nThe three readiness dimensions `configured` / `learned` / `indexed` are reported\nindependently. `indexed` may be `true`, `false`, or `null` — a `null` means the\nindex status could not be confirmed and must **not** be read as \"indexed\".\n\n## The integrations\n\n| Integration id | What it is | What it unlocks |\n| --- | --- | --- |\n| `jira` | Jira API access | Ticket reads/writes, estimation and review automations, status transitions. |\n| `github_app` | GitHub App installation | Pull requests, code review, and private-repo parsing on GitHub projects. |\n| `vcs_access_token` | VCS access token | Pull requests, code review, and private-repo parsing on Bitbucket projects. |\n| `vcs_webhook` | VCS webhook secret | Merge webhooks and CI follow-up triggers. |\n| `code_index` | A successful repository index | Codebase-grounded planning, architecture, reimplementation, and technical/discovery brainstorms. Produced by `/parse-repository`. |\n\nA project's `github_app` **or** `vcs_access_token` provides the VCS connection;\nwhich one applies depends on the project's version-control system. When the\nproject's provider is unknown, either credential satisfies the requirement — the\nreport expresses that as `semantics: any_of`.\n\n## The gates, by capability\n\n### Pull requests and CI (BLOCK on VCS)\n\nTools like `create_pull_request`, `resolve_ci_checks`, `poll_ci_checks`, and\n`materialize_fresh_base` are **unavailable** (`BLOCK`) until a VCS connection is\nconfigured. They act directly on the version-control host, so without a\nconnection there is nothing for them to talk to.\n\n### Repository indexing and maps (BLOCK on VCS)\n\n`parse_repository` and `regenerate_directory_map` need a VCS connection to read\nthe repository. They **BLOCK** until VCS is connected.\n\n### Codebase-grounded generation (BLOCK on VCS **and** a code index)\n\nPlanning and architecture tools — `generate_plan_direct`,\n`generate_architecture_direct`, `request_reimplement_context`,\n`code_writer_generate_plan`, `code_writer_generate_architecture`, and\n`create_doc` for **TDD** / **architecture** documents — ground their output in\nyour indexed codebase. They **BLOCK** until BOTH a VCS connection AND a\nsuccessful code index exist (`all_of`, with `code_index` in `missing`).\n\n### Council (BLOCK on a code index, mode-dependent)\n\n`request_council` in **technical** or **discovery** mode searches your indexed\ncodebase, so it **BLOCK**s on `code_index`. **Design**-mode brainstorming never\nqueries the index and is never gated.\n\n### Document generation that DEGRADEs (usable without codebase context)\n\nTools like `generate_prd_direct`, `generate_fsd_direct`,\n`code_writer_generate_fsd`, `generate_clarifying_questions_direct`,\n`generate_ticket_critique_direct`, `generate_ticket_review_direct`, and\n`create_doc` for **PRD** / **FSD** documents **DEGRADE** rather than block: they\nrun today from the ticket alone, and connecting VCS simply lets them ground their\noutput in your codebase. They always appear under \"Tools you can use now\", with a\nreduced-context caveat when the VCS connection is missing.\n\n### Never gated\n\nSetup and bootstrap tools (`ping`, `config_field`, `get_install_manifest`,\n`apply_install_manifest`, `get_my_role`, `persist_routing_credential`,\n`get_docs_dir`, and the bootstrap-invite exchange) are always available — they\nare how you configure everything else.\n",
|
|
5
|
+
"docs/install/sfcc-integration.md": "# Installing the SFCC Integration (OCAPI)\n\nBridge's Salesforce B2C Commerce (SFCC) tools give an AI coding agent read access to\na sandbox's object model, custom object definitions, and site preferences — plus a\nsmall set of sandbox-only writes — through the **OCAPI Data API**. This guide covers\nsetting up the OCAPI client that those tools authenticate against.\n\n> **Sandbox / local development only.** This integration is intended for a **developer\n> sandbox**. The tools reject non-sandbox instances, and the permissions grant below is\n> deliberately broad (all methods, all resources) — appropriate for a throwaway dev\n> sandbox, **never** for staging or production. Do not configure this grant on any\n> instance that holds real data. Credentials stay local (in `dw.json` or `SFCC_*` env\n> vars) and are never sent to Bridge.\n\nFor the full per-tool list and what each SFCC tool depends on, see\n[MCP Tool Integration Dependencies](./mcp-tool-integrations.md). For the tool reference\nand the `BRIDGE_MCP_PROFILE` gating, see the SFCC section of the\n[package README](../../README.md).\n\n## Prerequisites\n\n- A running SFCC **developer sandbox** and its hostname\n (e.g. `zzzz-001.sandbox.us01.dx.commercecloud.salesforce.com`).\n- An **Account Manager API client** — a `client-id` and `client-secret`. This is the\n OCAPI client the tools use to obtain an OAuth token. Create one in Account Manager\n (**API Client** → *Add API Client*) if you don't already have it, and note its\n `client_id`.\n- Business Manager access to the sandbox with permission to edit **Open Commerce API\n Settings**.\n\n## 1. Grant the OCAPI client access in Business Manager\n\nIn Business Manager for the sandbox:\n\n**Administration → Site Development → Open Commerce API Settings → Data API** tab.\n\nAdd the following client entry to the `clients` array of the Data API settings, then\n**Save**. This grants the client full read/write access to every Data API resource —\nacceptable only on a developer sandbox:\n\n```json\n{\n \"client_id\": \"<your-client-id-here>\",\n \"resources\": [\n {\n \"methods\": [\"get\", \"post\", \"put\", \"patch\", \"delete\"],\n \"read_attributes\": \"(**)\",\n \"write_attributes\": \"(**)\",\n \"resource_id\": \"/**\"\n }\n ]\n}\n```\n\nNotes:\n\n- The `client_id` **must match** the Account Manager API client whose credentials you\n put in `dw.json` / `SFCC_*` below. Replace the value above with your own client id if\n it differs.\n- If the Data API settings are empty, wrap the entry in the standard settings envelope:\n\n ```json\n {\n \"_v\": \"23.2\",\n \"clients\": [\n {\n \"client_id\": \"<your-client-id-here>\",\n \"resources\": [\n {\n \"methods\": [\"get\", \"post\", \"put\", \"patch\", \"delete\"],\n \"read_attributes\": \"(**)\",\n \"write_attributes\": \"(**)\",\n \"resource_id\": \"/**\"\n }\n ]\n }\n ]\n }\n ```\n\n- `check_permissions` (below) prints a ready-to-paste grant JSON on a 401/403, so you can\n also let the tool tell you exactly what to add.\n\n## 2. Provide credentials locally\n\nCreate a `dw.json` in your project root (auto-added to git exclude — never commit it):\n\n```json\n{\n \"hostname\": \"zzzz-001.sandbox.us01.dx.commercecloud.salesforce.com\",\n \"client-id\": \"<your-client-id-here>\",\n \"client-secret\": \"<account-manager-client-secret>\"\n}\n```\n\nAccepted key spellings: `hostname`/`host`, `client-id`/`clientId`/`client_id`,\n`client-secret`/`clientSecret`/`client_secret`. Prefer a single config — a multi-entry\n`configs[]` array forces an explicit `instance` on every call. Alternatively, export\n`SFCC_HOSTNAME` / `SFCC_CLIENT_ID` / `SFCC_CLIENT_SECRET` in the MCP server environment.\n\n## 3. Set the repo `version` config field\n\nSet the repo's `version` config to your SFCC project type — one of\n`sfra | pwakit | sitegenesis | storefrontnext | hybrid`. The call-time gate reads this;\na non-SFCC value blocks every SFCC tool except `sfcc_setup_status`. Set it via your\nnormal config path, the `config_field` MCP tool (operation `update`, field `version`),\nor the `/teach-bridge` skill.\n\n## 4. Enable the SFCC tools\n\nThe two diagnostic tools (`sfcc_setup_status`, `check_permissions`) are always\nregistered. The read tools, write tools, and `sfcc_log_query` are gated behind the\n`sfcc` profile. Add `sfcc` to `BRIDGE_MCP_PROFILE` in the MCP server `env` block (it is\ncomma-separated; `full` also works), then **restart the MCP client**:\n\n```json\n\"env\": { \"BRIDGE_MCP_PROFILE\": \"sfcc\" }\n```\n\n## 5. Verify\n\nAsk your agent to run:\n\n1. `sfcc_setup_status` — expect all prerequisite checks ✓ (Bridge API key, repo name,\n `version` config, `dw.json` presence/uniqueness, AM/OCAPI token acquisition).\n2. `check_permissions` — probes OCAPI via `GET /system_object_definitions`. A 200 (with\n the OCAPI version) confirms the grant. On 401/403 it prints the exact grant JSON to\n paste back in step 1.\n\nRestart the MCP client after any credential, grant, or env change — a running session\ndoes not pick them up.\n\n## Notes\n\n- **WebDAV logs are separate.** `sfcc_log_query` authenticates with a Business Manager\n username + a 40-character **WebDAV access key** over HTTP Basic auth — *not* the OCAPI\n OAuth token configured here. `sfcc_setup_status` reports OCAPI (step 5) and WebDAV\n (step 6) independently; one can be green while the other is not.\n- **Writes are sandbox-only.** The write tools (attribute/preference create/update) target\n a developer sandbox and echo a paste-ready grant JSON on a 403.\n"
|
|
5
6
|
};
|
package/build/doctor.js
CHANGED
|
@@ -20,7 +20,11 @@ import os from "os";
|
|
|
20
20
|
import path from "path";
|
|
21
21
|
import { createDefaultStartTicketsDeps } from "./start-tickets.js";
|
|
22
22
|
import { VERSION } from "./version.generated.js";
|
|
23
|
-
import { collectInstallStatusChecks, formatInstallStatusReport, } from "./install-doctor.js";
|
|
23
|
+
import { collectInstallStatusChecks, formatInstallStatusReport, resolveInstallDoctorTarget, } from "./install-doctor.js";
|
|
24
|
+
import { parseDefaultOnEnvFlag } from "./env-flags.js";
|
|
25
|
+
import { createBridgeApiUrls } from "./bridge-api-urls.js";
|
|
26
|
+
import { probeToolSurface } from "./tool-surface-gating.js";
|
|
27
|
+
import { resolveBapiCredentials } from "./credential-store.js";
|
|
24
28
|
import { DEFAULT_AGENT_NAME, resolveAgentSpec, isAgentName, formatValidAgentNames, } from "./agent-registry.js";
|
|
25
29
|
import { getDoctorPrereqDescriptors, probePrerequisite, } from "./start-tickets-prereqs.js";
|
|
26
30
|
import { resolveProfiles } from "./mcp-profile.js";
|
|
@@ -51,6 +55,14 @@ export function getDoctorUsage() {
|
|
|
51
55
|
"bootstrap-field completeness, and repository-indexing state. It performs",
|
|
52
56
|
"read-only GETs only and never affects the exit code.",
|
|
53
57
|
"",
|
|
58
|
+
"It also includes an advisory 'MCP tool surface' section (BAPI-641): what",
|
|
59
|
+
"dynamic capability gating would advertise for this repo. It performs at most",
|
|
60
|
+
"one read-only GET to /jira/mcp/tool-surface (none under the kill switch) and",
|
|
61
|
+
"is advisory/fail-open — a timeout or malformed response is reported as",
|
|
62
|
+
"'fail-open to full surface' and never affects the exit code. Clients that",
|
|
63
|
+
"ignore notifications/tools/list_changed must reconnect or start a new MCP",
|
|
64
|
+
"session to observe surface changes; no project MCP config change is required.",
|
|
65
|
+
"",
|
|
54
66
|
"Conductor ledger / native-module diagnostics (the SQLite ledger's native",
|
|
55
67
|
"binding load status and Node-version skew) live under a separate command:",
|
|
56
68
|
" conductor doctor",
|
|
@@ -363,6 +375,111 @@ export function formatLauncherCacheReport(inspections) {
|
|
|
363
375
|
}
|
|
364
376
|
return lines.join("\n");
|
|
365
377
|
}
|
|
378
|
+
/**
|
|
379
|
+
* Collect the read-only tool-surface diagnostic. Evaluates the kill switch first
|
|
380
|
+
* and returns without any network request when disabled. For an enabled flag and
|
|
381
|
+
* a resolved repo/credential, performs ONE 500 ms `probeToolSurface()` GET to the
|
|
382
|
+
* deployed route. Credential values live ONLY in the request headers and never in
|
|
383
|
+
* the returned result. Never throws.
|
|
384
|
+
*/
|
|
385
|
+
export async function collectToolSurfaceDiagnostic(deps) {
|
|
386
|
+
const enabled = parseDefaultOnEnvFlag(deps.env.BAPI_MCP_TOOL_SURFACE_GATING_ENABLED);
|
|
387
|
+
if (!enabled) {
|
|
388
|
+
return { enabled: false, reason: "kill-switch" };
|
|
389
|
+
}
|
|
390
|
+
const target = await resolveInstallDoctorTarget(deps);
|
|
391
|
+
if (!target.repoName) {
|
|
392
|
+
return {
|
|
393
|
+
enabled: true,
|
|
394
|
+
reason: "unresolved",
|
|
395
|
+
detail: "no BAPI_REPO_NAME in the environment or project-local MCP configs",
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
const credDeps = {
|
|
399
|
+
env: deps.env,
|
|
400
|
+
homedir: deps.homedir,
|
|
401
|
+
platform: deps.platform,
|
|
402
|
+
readFile: deps.readFile,
|
|
403
|
+
stat: deps.stat,
|
|
404
|
+
stderr: () => { },
|
|
405
|
+
};
|
|
406
|
+
const cred = await resolveBapiCredentials(target.repoName, credDeps);
|
|
407
|
+
if (!cred.ok) {
|
|
408
|
+
return {
|
|
409
|
+
enabled: true,
|
|
410
|
+
reason: "unresolved",
|
|
411
|
+
detail: `no Bridge API credential resolved (${cred.kind})`,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
const apiKey = cred.credentials.apiKey;
|
|
415
|
+
const urls = createBridgeApiUrls(target.baseUrl);
|
|
416
|
+
const url = urls.buildGetUrl("/mcp/tool-surface", {
|
|
417
|
+
repo_name: target.repoName,
|
|
418
|
+
});
|
|
419
|
+
const result = await probeToolSurface({
|
|
420
|
+
url,
|
|
421
|
+
resolveHeaders: async () => ({ "X-API-Key": apiKey }),
|
|
422
|
+
fetchFn: deps.fetch,
|
|
423
|
+
});
|
|
424
|
+
if (result.reason === "blocked") {
|
|
425
|
+
return {
|
|
426
|
+
enabled: true,
|
|
427
|
+
reason: "blocked",
|
|
428
|
+
blockedTools: Array.from(result.blockedTools),
|
|
429
|
+
catalogRevision: result.catalogRevision,
|
|
430
|
+
evaluatedToolCount: result.evaluatedToolCount,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
if (result.reason === "timeout") {
|
|
434
|
+
return { enabled: true, reason: "timeout" };
|
|
435
|
+
}
|
|
436
|
+
return { enabled: true, reason: "malformed", subtype: result.subtype };
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Render the advisory "MCP tool surface" section (pure formatting — no probing).
|
|
440
|
+
* States the kill-switch state, probe reachability, decision reason/subtype,
|
|
441
|
+
* fail-open behavior when applicable, blocked IDs/count/revision for a valid
|
|
442
|
+
* response, and clarifies that blocked IDs are intersected with the locally
|
|
443
|
+
* active profile and SDK-enabled baseline only when an MCP session starts.
|
|
444
|
+
*/
|
|
445
|
+
export function formatToolSurfaceDiagnosticReport(diag) {
|
|
446
|
+
const lines = ["", "MCP tool surface (dynamic capability gating — advisory)", ""];
|
|
447
|
+
lines.push(`Kill switch: ${diag.enabled ? "ENABLED (gating active)" : "DISABLED (full surface)"}`);
|
|
448
|
+
switch (diag.reason) {
|
|
449
|
+
case "kill-switch":
|
|
450
|
+
lines.push("Reason: kill-switch — BAPI_MCP_TOOL_SURFACE_GATING_ENABLED is off, so the full profile surface is advertised and no probe is performed.");
|
|
451
|
+
break;
|
|
452
|
+
case "unresolved":
|
|
453
|
+
lines.push(`Reason: unresolved — ${diag.detail ?? "repo/credential not resolved"}; the probe was skipped and the full surface is advertised (fail-open to full surface).`);
|
|
454
|
+
break;
|
|
455
|
+
case "blocked": {
|
|
456
|
+
const ids = diag.blockedTools ?? [];
|
|
457
|
+
lines.push("Reason: blocked — the backend returned a valid capability decision.");
|
|
458
|
+
lines.push(`Probe: reachable (HTTP 200, valid response).`);
|
|
459
|
+
lines.push(`Blocked tools (${ids.length}): [${ids.join(", ")}]`);
|
|
460
|
+
if (diag.catalogRevision)
|
|
461
|
+
lines.push(`Catalog revision: ${diag.catalogRevision}`);
|
|
462
|
+
if (typeof diag.evaluatedToolCount === "number") {
|
|
463
|
+
lines.push(`Evaluated tool count: ${diag.evaluatedToolCount}`);
|
|
464
|
+
}
|
|
465
|
+
break;
|
|
466
|
+
}
|
|
467
|
+
case "timeout":
|
|
468
|
+
lines.push("Reason: timeout — the 500 ms probe deadline elapsed; fail-open to full surface.");
|
|
469
|
+
break;
|
|
470
|
+
case "malformed":
|
|
471
|
+
lines.push(`Reason: malformed (${diag.subtype ?? "unknown"}) — fail-open to full surface.`);
|
|
472
|
+
break;
|
|
473
|
+
}
|
|
474
|
+
lines.push("");
|
|
475
|
+
lines.push("Blocked IDs are intersected with the locally active MCP profile and the current SDK-enabled");
|
|
476
|
+
lines.push("baseline only when an MCP session starts, so IDs unknown to this package or excluded by the");
|
|
477
|
+
lines.push("active profile have no effect. Capability-hidden tools remain registered and callable — the");
|
|
478
|
+
lines.push("backend is the enforcement boundary. This section is advisory and never changes the exit code.");
|
|
479
|
+
lines.push("Clients that ignore notifications/tools/list_changed must reconnect or start a new MCP session");
|
|
480
|
+
lines.push("to observe surface changes; no project MCP configuration change is required.");
|
|
481
|
+
return lines.join("\n");
|
|
482
|
+
}
|
|
366
483
|
/**
|
|
367
484
|
* CLI entry for the read-only `doctor` subcommand. Returns a process exit code.
|
|
368
485
|
* Help returns 0; parser errors return 1; otherwise it prints the report and
|
|
@@ -429,6 +546,36 @@ export async function runDoctorCli(argv, overrides = {}) {
|
|
|
429
546
|
/* install-status diagnostics are advisory; never block the doctor report */
|
|
430
547
|
}
|
|
431
548
|
}
|
|
549
|
+
// Advisory MCP tool-surface capability section (BAPI-641). Strictly read-only:
|
|
550
|
+
// one 500 ms GET (or none, under the kill switch). Any timeout, malformed
|
|
551
|
+
// response, or unexpected throw degrades to a "fail-open to full surface" line
|
|
552
|
+
// and never affects the exit code, exactly like install-status above.
|
|
553
|
+
if (overrides.toolSurface !== false) {
|
|
554
|
+
try {
|
|
555
|
+
const injectedFs = deps;
|
|
556
|
+
const toolSurfaceDeps = {
|
|
557
|
+
env: overrides.toolSurface?.env ?? deps.env,
|
|
558
|
+
cwd: overrides.toolSurface?.cwd ?? deps.cwd,
|
|
559
|
+
platform: overrides.toolSurface?.platform ?? deps.platform,
|
|
560
|
+
homedir: overrides.toolSurface?.homedir ?? injectedFs.homedir ?? os.homedir,
|
|
561
|
+
readFile: overrides.toolSurface?.readFile ??
|
|
562
|
+
injectedFs.readFile ??
|
|
563
|
+
((p) => readFile(p, "utf-8")),
|
|
564
|
+
stat: overrides.toolSurface?.stat ?? injectedFs.stat ?? ((p) => stat(p)),
|
|
565
|
+
fetch: overrides.toolSurface?.fetch ?? ((...args) => fetch(...args)),
|
|
566
|
+
};
|
|
567
|
+
const diagnostic = await collectToolSurfaceDiagnostic(toolSurfaceDeps);
|
|
568
|
+
log(formatToolSurfaceDiagnosticReport(diagnostic));
|
|
569
|
+
}
|
|
570
|
+
catch {
|
|
571
|
+
// Any unexpected failure still degrades to a sanitized advisory section.
|
|
572
|
+
log(formatToolSurfaceDiagnosticReport({
|
|
573
|
+
enabled: parseDefaultOnEnvFlag(deps.env.BAPI_MCP_TOOL_SURFACE_GATING_ENABLED),
|
|
574
|
+
reason: "malformed",
|
|
575
|
+
subtype: "unexpected",
|
|
576
|
+
}));
|
|
577
|
+
}
|
|
578
|
+
}
|
|
432
579
|
if (!collection.ok)
|
|
433
580
|
return 1;
|
|
434
581
|
return collection.results.some((r) => !r.found) ? 1 : 0;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* env-flags — shared parsing for default-ON boolean environment flags.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from `index.ts` (BAPI-641) so normal server startup and the
|
|
5
|
+
* `doctor` diagnostic share ONE implementation of the default-on semantics.
|
|
6
|
+
* A default-on flag is enabled unless explicitly set to a recognized off-token,
|
|
7
|
+
* so an unknown value fails OPEN (enabled) rather than silently disabling a
|
|
8
|
+
* feature.
|
|
9
|
+
*/
|
|
10
|
+
/** The recognized, case-insensitive off-tokens for a default-on flag. */
|
|
11
|
+
const OFF_TOKENS = new Set([
|
|
12
|
+
"false",
|
|
13
|
+
"0",
|
|
14
|
+
"no",
|
|
15
|
+
"off",
|
|
16
|
+
"disabled",
|
|
17
|
+
]);
|
|
18
|
+
/**
|
|
19
|
+
* Parse a default-ON boolean env flag. `undefined` / blank → true; the
|
|
20
|
+
* normalized off-tokens (`false`, `0`, `no`, `off`, `disabled`) → false; any
|
|
21
|
+
* other value → true (preserving default-on / fail-open behavior). Matching is
|
|
22
|
+
* case-insensitive and trims surrounding whitespace.
|
|
23
|
+
*/
|
|
24
|
+
export function parseDefaultOnEnvFlag(value) {
|
|
25
|
+
if (value === undefined)
|
|
26
|
+
return true;
|
|
27
|
+
const normalized = value.trim().toLowerCase();
|
|
28
|
+
if (normalized === "")
|
|
29
|
+
return true;
|
|
30
|
+
return !OFF_TOKENS.has(normalized);
|
|
31
|
+
}
|