@bridge_gpt/mcp-server 0.2.24 → 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.
- package/README.md +39 -19
- package/build/commands.generated.js +3 -3
- package/build/conductor/epic-reconcile.js +7 -1
- package/build/conductor/epic-runtime.js +5 -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/index.js +1124 -368
- package/build/install-bridge.js +278 -34
- package/build/install-doctor.js +64 -0
- package/build/pipelines.generated.js +122 -128
- package/build/readme.generated.js +1 -1
- package/build/start-tickets.js +48 -13
- package/build/version.generated.js +1 -1
- package/docs/install/github-app.md +80 -17
- package/package.json +2 -2
- 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 +187 -73
- package/public/js/main.min.js.map +1 -1
|
@@ -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
|
};
|