@bridge_gpt/mcp-server 0.2.23 → 0.2.25

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.
@@ -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
- const needsForm = hasDecisions || (isPlanning && (data.system_goals?.nfrs?.length ?? 0) > 0);
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
- .nfr-feedback .comment-area {
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,19 +589,87 @@ function renderNoDecisions(isPlanning = false) {
583
589
  <p>${escapeHtml(message)}</p>
584
590
  </div>`;
585
591
  }
586
- // Read-only system-goals panel for the pre_ticket_planning artifact. Returns ""
587
- // when no goals are supplied so the review_decisions page is byte-for-byte
588
- // unchanged. Every model-supplied string is escaped via escapeHtml().
589
- // When isPlanning is true, each NFR renders an interactive stance control
590
- // (Agreed / Ask about this / Disagree) with a conditional comment textarea.
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 &amp; System Goals"
666
+ : "System Goals &amp; Non-Functional Requirements";
594
667
  const nfrs = goals.nfrs ?? [];
595
668
  let nfrHtml = "";
596
669
  if (nfrs.length > 0) {
597
670
  let items = "";
598
- for (const nfr of nfrs) {
671
+ for (let i = 0; i < nfrs.length; i++) {
672
+ const nfr = nfrs[i];
599
673
  const statusClass = nfr.status === "confirmed"
600
674
  ? "status-confirmed"
601
675
  : nfr.status === "assumed"
@@ -603,11 +677,15 @@ function renderSystemGoals(goals, isPlanning = false) {
603
677
  : "status-open";
604
678
  // nfrId is used as data-nfr-id (becomes the JSON output key) and aria-label.
605
679
  // nfrHtmlId is a whitespace-free variant used for id/name/for attributes —
606
- // HTML5 forbids spaces in id values.
680
+ // HTML5 forbids spaces in id values. The loop index i prefixes the radio/
681
+ // textarea identifiers so each requirement is its own radio group: without
682
+ // it, two categories that collapse to the same slug (e.g. "performance
683
+ // latency" vs "performance-latency") would share a name and clobber each
684
+ // other's selection. Mirrors renderImplementationOrder's index disambiguation.
607
685
  const nfrId = escapeHtml(nfr.category);
608
686
  const nfrHtmlId = escapeHtml(nfr.category.replace(/\s+/g, "-"));
609
- const radioName = `nfr-stance-${nfrHtmlId}`;
610
- const textareaId = `nfr-comment-${nfrHtmlId}`;
687
+ const radioName = `nfr-stance-${i}-${nfrHtmlId}`;
688
+ const textareaId = `nfr-comment-${i}-${nfrHtmlId}`;
611
689
  const stanceControls = isPlanning ? `
612
690
  <div class="nfr-feedback" data-nfr-id="${nfrId}">
613
691
  <div class="radio-group" role="radiogroup" aria-label="Stance on ${nfrId}">
@@ -642,7 +720,7 @@ function renderSystemGoals(goals, isPlanning = false) {
642
720
  </ul>`;
643
721
  }
644
722
  return ` <section class="planning-section" data-testid="system-goals">
645
- <h2>System Goals &amp; Non-Functional Requirements</h2>
723
+ <h2>${sectionHeading}</h2>
646
724
  <div class="goal-row">
647
725
  <div class="goal-label">Business goal</div>
648
726
  <div class="goal-body" data-testid="system-goal-business">${escapeHtml(goals.business_goal)}</div>
@@ -654,7 +732,7 @@ function renderSystemGoals(goals, isPlanning = false) {
654
732
  <div class="goal-row">
655
733
  <div class="goal-label">System behavior</div>
656
734
  <div class="goal-body" data-testid="system-goal-behavior">${escapeHtml(goals.system_behavior)}</div>
657
- </div>${nfrHtml}
735
+ </div>${acHtml}${nfrHtml}
658
736
  </section>`;
659
737
  }
660
738
  // Read-only recommended implementation order (epic surfaces). Returns "" when the
@@ -848,9 +926,11 @@ function renderScript(data, isPlanning = false) {
848
926
  postSubmitContainer = document.getElementById("post-submit-container");
849
927
  jsonOutput = document.getElementById("json-output");
850
928
 
851
- ${isPlanning ? `// NFR stance radio disclosure (planning mode only)
852
- var nfrContainers = document.querySelectorAll(".nfr-feedback");
853
- nfrContainers.forEach(function(container) {
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) {
854
934
  var radios = container.querySelectorAll('input[type="radio"]');
855
935
  radios.forEach(function(radio) {
856
936
  radio.addEventListener("change", function() {
@@ -869,7 +949,7 @@ function renderScript(data, isPlanning = false) {
869
949
  }
870
950
  });
871
951
  });
872
- });` : `var nfrContainers = [];`}
952
+ });` : `var stanceContainers = [];`}
873
953
 
874
954
  submitBtn.addEventListener("click", function() {
875
955
  var cards = document.querySelectorAll(".card[data-item-id]");
@@ -904,8 +984,8 @@ function renderScript(data, isPlanning = false) {
904
984
  }
905
985
  });
906
986
 
907
- // Validate NFR feedback: ask/disagree stance requires a comment.
908
- nfrContainers.forEach(function(container) {
987
+ // Validate stance feedback: ask/disagree requires a comment.
988
+ stanceContainers.forEach(function(container) {
909
989
  var selected = container.querySelector('input[type="radio"]:checked');
910
990
  if (!selected) return;
911
991
  if (selected.value === "ask" || selected.value === "disagree") {
@@ -952,22 +1032,29 @@ function renderScript(data, isPlanning = false) {
952
1032
  };
953
1033
  });
954
1034
 
955
- ${isPlanning ? `// Capture NFR feedback stances
956
- var nfrFeedback = {};
957
- nfrContainers.forEach(function(container) {
958
- var nfrId = container.getAttribute("data-nfr-id");
959
- var selected = container.querySelector('input[type="radio"]:checked');
960
- var commentArea = container.querySelector(".comment-area");
961
- var textarea = commentArea ? commentArea.querySelector("textarea") : null;
962
- nfrFeedback[nfrId] = {
963
- stance: selected ? selected.value : "agreed",
964
- comment: textarea ? textarea.value : ""
965
- };
966
- });` : ""}
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");` : ""}
967
1053
 
968
1054
  var output = {
969
1055
  ticket_key: ${safeJsonForScript(data.ticket_key)},
970
1056
  decisions: decisions,
1057
+ ${isPlanning ? "acceptance_criteria_feedback: acceptanceCriteriaFeedback," : ""}
971
1058
  ${isPlanning ? "nfr_feedback: nfrFeedback," : ""}
972
1059
  general_comment: document.getElementById("general-comment").value
973
1060
  };
@@ -0,0 +1,5 @@
1
+ // AUTO-GENERATED — do not edit manually. Regenerate with: npm run build
2
+ // This file is produced by scripts/bundle-docs.js
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### Brainstorms (BLOCK on a code index, mode-dependent)\n\n`request_brainstorm` 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
+ };