@bridge_gpt/mcp-server 0.1.12 → 0.1.13
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/build/commands.generated.js +1 -1
- package/build/decision-page-schema.js +101 -0
- package/build/decision-page-schema.test.js +248 -0
- package/build/decision-page-template.js +222 -109
- package/build/index.js +19 -33
- package/build/pipelines.generated.js +15 -35
- package/build/version.generated.js +1 -1
- package/package.json +1 -1
- package/pipelines/check-ci-ticket.json +3 -8
- package/pipelines/implement-ticket.json +3 -8
- package/pipelines/pr-ticket.json +3 -8
- package/pipelines/review-ticket.json +3 -8
|
@@ -25,6 +25,13 @@ function safeJsonForScript(value) {
|
|
|
25
25
|
return JSON.stringify(value).replace(/</g, "\\u003c");
|
|
26
26
|
}
|
|
27
27
|
// ---------------------------------------------------------------------------
|
|
28
|
+
// UI copy constants
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
const COPY_SUCCESS_LABEL = "Copied!";
|
|
31
|
+
const COPY_FALLBACK_PROMPT_LABEL = "Auto-copy unavailable. Press Ctrl+C / Cmd+C to copy.";
|
|
32
|
+
const PAGE_INTRO_ASK_COPY = "Have a question about an item? Choose 'Ask about this' for that card; we'll discuss before the changes are made.";
|
|
33
|
+
const PAGE_INTRO_NO_DECISIONS = "All suggestions were confirmed as improvements. No decisions are needed from you.";
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
28
35
|
// Template
|
|
29
36
|
// ---------------------------------------------------------------------------
|
|
30
37
|
const DEFAULT_ASSETS = { faviconBase64: "", logoBase64: "", fontsRelPath: "" };
|
|
@@ -35,6 +42,7 @@ export function generateDecisionPageHtml(data, assets = DEFAULT_ASSETS) {
|
|
|
35
42
|
? `<link rel="icon" type="image/png" sizes="32x32" href="data:image/png;base64,${assets.faviconBase64}">`
|
|
36
43
|
: "";
|
|
37
44
|
const fontFaces = assets.fontsRelPath ? renderFontFaces(assets.fontsRelPath) : "";
|
|
45
|
+
const pageIntro = hasDecisions ? PAGE_INTRO_ASK_COPY : PAGE_INTRO_NO_DECISIONS;
|
|
38
46
|
return `<!DOCTYPE html>
|
|
39
47
|
<html lang="en">
|
|
40
48
|
<head>
|
|
@@ -143,14 +151,20 @@ ${fontFaces}
|
|
|
143
151
|
font-weight: 600;
|
|
144
152
|
margin: 0 0 0.5rem 0;
|
|
145
153
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
border-left: 4px solid var(--primary-color);
|
|
149
|
-
padding: 0.75rem 1rem;
|
|
154
|
+
/* Card sub-sections (Original question, Why it matters, Why the AI recommended this) */
|
|
155
|
+
.card-section {
|
|
150
156
|
margin-bottom: 1rem;
|
|
157
|
+
}
|
|
158
|
+
.card-section-label {
|
|
151
159
|
font-size: 0.875rem;
|
|
152
|
-
|
|
153
|
-
|
|
160
|
+
font-weight: 600;
|
|
161
|
+
color: var(--secondary-color);
|
|
162
|
+
margin-bottom: 0.25rem;
|
|
163
|
+
text-transform: none;
|
|
164
|
+
}
|
|
165
|
+
.card-section-body {
|
|
166
|
+
font-size: 0.95rem;
|
|
167
|
+
color: var(--text-color);
|
|
154
168
|
white-space: pre-wrap;
|
|
155
169
|
}
|
|
156
170
|
|
|
@@ -158,8 +172,13 @@ ${fontFaces}
|
|
|
158
172
|
.radio-group {
|
|
159
173
|
display: flex;
|
|
160
174
|
flex-direction: column;
|
|
161
|
-
gap: 0.
|
|
162
|
-
margin-bottom:
|
|
175
|
+
gap: 0.75rem;
|
|
176
|
+
margin-bottom: 1rem;
|
|
177
|
+
}
|
|
178
|
+
.radio-option-wrapper {
|
|
179
|
+
display: flex;
|
|
180
|
+
flex-direction: column;
|
|
181
|
+
gap: 0.25rem;
|
|
163
182
|
}
|
|
164
183
|
.radio-option {
|
|
165
184
|
display: flex;
|
|
@@ -176,9 +195,42 @@ ${fontFaces}
|
|
|
176
195
|
font-weight: 400;
|
|
177
196
|
margin-bottom: 0;
|
|
178
197
|
}
|
|
198
|
+
.radio-option-consequence {
|
|
199
|
+
font-size: 0.9rem;
|
|
200
|
+
color: var(--secondary-color);
|
|
201
|
+
margin-left: 1.75rem;
|
|
202
|
+
white-space: pre-wrap;
|
|
203
|
+
}
|
|
179
204
|
.ai-recommendation {
|
|
180
205
|
color: var(--primary-color);
|
|
181
206
|
font-weight: 600;
|
|
207
|
+
white-space: nowrap;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/* Codebase evidence disclosure */
|
|
211
|
+
.codebase-evidence {
|
|
212
|
+
margin-bottom: 1rem;
|
|
213
|
+
border: 1px solid var(--border-color);
|
|
214
|
+
border-radius: 0.25rem;
|
|
215
|
+
background: #fafafa;
|
|
216
|
+
}
|
|
217
|
+
.codebase-evidence summary {
|
|
218
|
+
cursor: pointer;
|
|
219
|
+
padding: 0.5rem 0.75rem;
|
|
220
|
+
font-size: 0.875rem;
|
|
221
|
+
font-weight: 600;
|
|
222
|
+
color: var(--secondary-color);
|
|
223
|
+
user-select: none;
|
|
224
|
+
}
|
|
225
|
+
.codebase-evidence summary:hover {
|
|
226
|
+
color: var(--text-color);
|
|
227
|
+
}
|
|
228
|
+
.codebase-evidence-body {
|
|
229
|
+
padding: 0.75rem 1rem;
|
|
230
|
+
border-top: 1px solid var(--border-color);
|
|
231
|
+
font-size: 0.875rem;
|
|
232
|
+
color: #374151;
|
|
233
|
+
white-space: pre-wrap;
|
|
182
234
|
}
|
|
183
235
|
|
|
184
236
|
/* Comment textareas */
|
|
@@ -356,7 +408,7 @@ ${renderHeader(assets)}
|
|
|
356
408
|
<main class="main-content">
|
|
357
409
|
<div class="container">
|
|
358
410
|
<h1>Review Decisions: ${escapeHtml(ticket_key)}</h1>
|
|
359
|
-
<p class="page-intro"
|
|
411
|
+
<p class="page-intro">${escapeHtml(pageIntro)}</p>
|
|
360
412
|
|
|
361
413
|
${hasDecisions ? renderForm(data) : renderNoDecisions()}
|
|
362
414
|
|
|
@@ -371,6 +423,13 @@ ${hasDecisions ? renderScript(data) : ""}
|
|
|
371
423
|
// ---------------------------------------------------------------------------
|
|
372
424
|
// Font faces
|
|
373
425
|
// ---------------------------------------------------------------------------
|
|
426
|
+
// fontsRelPath comes from server-controlled env vars, so this is not an
|
|
427
|
+
// injection vector — but a deploy path containing a single quote or backslash
|
|
428
|
+
// would silently break CSS parsing and stop fonts from loading. Escape both
|
|
429
|
+
// so the url('...') stays well-formed regardless of deploy layout.
|
|
430
|
+
function escapeCssUrl(value) {
|
|
431
|
+
return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
432
|
+
}
|
|
374
433
|
function renderFontFaces(fontsRelPath) {
|
|
375
434
|
const weights = [
|
|
376
435
|
{ weight: "400", style: "normal", file: "SourceSansPro-Regular.ttf" },
|
|
@@ -380,9 +439,10 @@ function renderFontFaces(fontsRelPath) {
|
|
|
380
439
|
{ weight: "700", style: "normal", file: "SourceSansPro-Bold.ttf" },
|
|
381
440
|
{ weight: "700", style: "italic", file: "SourceSansPro-BoldItalic.ttf" },
|
|
382
441
|
];
|
|
442
|
+
const safePath = escapeCssUrl(fontsRelPath);
|
|
383
443
|
return weights.map(({ weight, style, file }) => ` @font-face {
|
|
384
444
|
font-family: 'Source Sans Pro';
|
|
385
|
-
src: url('${
|
|
445
|
+
src: url('${safePath}/${escapeCssUrl(file)}') format('truetype');
|
|
386
446
|
font-weight: ${weight};
|
|
387
447
|
font-style: ${style};
|
|
388
448
|
}`).join("\n");
|
|
@@ -420,11 +480,11 @@ function renderForm(data) {
|
|
|
420
480
|
html += `
|
|
421
481
|
<div class="general-comment-area">
|
|
422
482
|
<label for="general-comment">General Comment (optional)</label>
|
|
423
|
-
<textarea id="general-comment" name="general_comment" placeholder="Any overall feedback or guidance..."></textarea>
|
|
483
|
+
<textarea id="general-comment" name="general_comment" placeholder="Any overall feedback or guidance..." data-testid="general-comment"></textarea>
|
|
424
484
|
</div>
|
|
425
485
|
|
|
426
486
|
<div class="submit-area">
|
|
427
|
-
<button type="button" id="submit-btn" class="btn btn-primary">Submit Decisions</button>
|
|
487
|
+
<button type="button" id="submit-btn" class="btn btn-primary" data-testid="submit-decisions">Submit Decisions</button>
|
|
428
488
|
</div>
|
|
429
489
|
</form>
|
|
430
490
|
</div>
|
|
@@ -435,7 +495,7 @@ function renderForm(data) {
|
|
|
435
495
|
<button type="button" class="btn btn-secondary" data-copy-json-btn data-default-label="Copy JSON">Copy JSON</button>
|
|
436
496
|
</div>
|
|
437
497
|
<p>Copy the JSON below and paste it back to the agent.</p>
|
|
438
|
-
<pre id="json-output"></pre>
|
|
498
|
+
<pre id="json-output" data-testid="decision-output-json"></pre>
|
|
439
499
|
<div class="copy-area">
|
|
440
500
|
<button type="button" id="copy-btn" class="btn btn-secondary" data-copy-json-btn data-default-label="Copy to Clipboard">Copy to Clipboard</button>
|
|
441
501
|
</div>
|
|
@@ -446,39 +506,74 @@ function renderDecisionItem(item) {
|
|
|
446
506
|
const id = escapeHtml(item.id);
|
|
447
507
|
const radioName = `radio-${id}`;
|
|
448
508
|
const textareaId = `comment-${id}`;
|
|
509
|
+
const titleId = `card-title-${id}`;
|
|
510
|
+
const originalQuestionLabelId = `original-question-label-${id}`;
|
|
511
|
+
const whyItMattersLabelId = `why-it-matters-label-${id}`;
|
|
512
|
+
const radioGroupLabelId = `radio-group-label-${id}`;
|
|
513
|
+
const recommendationLabelId = `recommendation-label-${id}`;
|
|
449
514
|
let optionsHtml = "";
|
|
450
515
|
for (let i = 0; i < item.options.length; i++) {
|
|
451
516
|
const val = `opt-${i}`;
|
|
452
517
|
const label = item.options[i];
|
|
518
|
+
const consequence = item.option_consequences[i] ?? "";
|
|
453
519
|
const isRecommended = i === item.recommendation_index;
|
|
454
520
|
const checked = isRecommended ? " checked" : "";
|
|
455
|
-
const
|
|
456
|
-
const
|
|
521
|
+
const labelText = escapeHtml(label);
|
|
522
|
+
const recommendationMarker = isRecommended
|
|
523
|
+
? ` <span class="ai-recommendation" data-testid="ai-recommendation-marker">(AI recommended)</span>`
|
|
524
|
+
: "";
|
|
457
525
|
optionsHtml += `
|
|
458
|
-
<div class="radio-option">
|
|
459
|
-
<
|
|
460
|
-
|
|
526
|
+
<div class="radio-option-wrapper" data-testid="option-wrapper" data-option-index="${i}">
|
|
527
|
+
<div class="radio-option">
|
|
528
|
+
<input type="radio" id="${radioName}-${val}" name="${radioName}" value="${val}"${checked} data-testid="decision-option">
|
|
529
|
+
<label for="${radioName}-${val}">${labelText}</label>${recommendationMarker}
|
|
530
|
+
</div>
|
|
531
|
+
<div class="radio-option-consequence" data-testid="option-consequence">${escapeHtml(consequence)}</div>
|
|
461
532
|
</div>`;
|
|
462
533
|
}
|
|
463
|
-
// Auto-append "None of these"
|
|
534
|
+
// Auto-append "Ask about this" then "None of these" — neither has a consequence line.
|
|
464
535
|
optionsHtml += `
|
|
465
|
-
<div class="radio-option">
|
|
466
|
-
<
|
|
467
|
-
|
|
536
|
+
<div class="radio-option-wrapper" data-testid="option-wrapper" data-option-index="ask">
|
|
537
|
+
<div class="radio-option">
|
|
538
|
+
<input type="radio" id="${radioName}-ask" name="${radioName}" value="ask" data-testid="decision-option">
|
|
539
|
+
<label for="${radioName}-ask">Ask about this</label>
|
|
540
|
+
</div>
|
|
541
|
+
</div>
|
|
542
|
+
<div class="radio-option-wrapper" data-testid="option-wrapper" data-option-index="none">
|
|
543
|
+
<div class="radio-option">
|
|
544
|
+
<input type="radio" id="${radioName}-none" name="${radioName}" value="none" data-testid="decision-option">
|
|
545
|
+
<label for="${radioName}-none">None of these</label>
|
|
546
|
+
</div>
|
|
468
547
|
</div>`;
|
|
469
548
|
// Encode option labels for client-side JS to resolve chosen_label
|
|
470
549
|
const labelsJson = escapeHtml(JSON.stringify(item.options));
|
|
471
550
|
return `
|
|
472
|
-
<
|
|
473
|
-
<
|
|
474
|
-
<
|
|
475
|
-
|
|
476
|
-
|
|
551
|
+
<section class="card" data-item-id="${id}" data-item-type="decision" data-source="${escapeHtml(item.source)}" data-labels="${labelsJson}" data-testid="decision-card" aria-labelledby="${titleId}">
|
|
552
|
+
<h3 class="card-title" id="${titleId}" data-testid="decision-card-title">${escapeHtml(item.question)}</h3>
|
|
553
|
+
<section class="card-section" data-testid="original-question-section" aria-labelledby="${originalQuestionLabelId}">
|
|
554
|
+
<div class="card-section-label" id="${originalQuestionLabelId}">Original question</div>
|
|
555
|
+
<div class="card-section-body" data-testid="original-question-body">${escapeHtml(item.original_question)}</div>
|
|
556
|
+
</section>
|
|
557
|
+
<section class="card-section" data-testid="why-it-matters-section" aria-labelledby="${whyItMattersLabelId}">
|
|
558
|
+
<div class="card-section-label" id="${whyItMattersLabelId}">Why it matters</div>
|
|
559
|
+
<div class="card-section-body" data-testid="why-it-matters-body">${escapeHtml(item.why_it_matters)}</div>
|
|
560
|
+
</section>
|
|
561
|
+
<section class="radio-group" role="radiogroup" data-testid="decision-radio-group" aria-labelledby="${radioGroupLabelId}">
|
|
562
|
+
<div class="card-section-label" id="${radioGroupLabelId}">Choose an option</div>${optionsHtml}
|
|
563
|
+
</section>
|
|
564
|
+
<section class="card-section" data-testid="recommendation-explanation-section" aria-labelledby="${recommendationLabelId}">
|
|
565
|
+
<div class="card-section-label" id="${recommendationLabelId}">Why the AI recommended this</div>
|
|
566
|
+
<div class="card-section-body" data-testid="recommendation-explanation-body">${escapeHtml(item.recommendation_explanation)}</div>
|
|
567
|
+
</section>
|
|
477
568
|
<div class="comment-area">
|
|
478
569
|
<label for="${textareaId}">Comment</label>
|
|
479
|
-
<textarea id="${textareaId}" name="${textareaId}" placeholder="Required for None of these selections..."></textarea>
|
|
570
|
+
<textarea id="${textareaId}" name="${textareaId}" placeholder="Required for None of these selections..." data-testid="decision-comment"></textarea>
|
|
480
571
|
</div>
|
|
481
|
-
|
|
572
|
+
<details class="codebase-evidence" data-testid="codebase-evidence">
|
|
573
|
+
<summary>Codebase evidence</summary>
|
|
574
|
+
<div class="codebase-evidence-body" data-testid="codebase-evidence-body">${escapeHtml(item.codebase_evidence)}</div>
|
|
575
|
+
</details>
|
|
576
|
+
</section>`;
|
|
482
577
|
}
|
|
483
578
|
function renderImprovements(improvements) {
|
|
484
579
|
if (improvements.length === 0)
|
|
@@ -491,16 +586,18 @@ function renderImprovements(improvements) {
|
|
|
491
586
|
? "confidence-medium"
|
|
492
587
|
: "confidence-low";
|
|
493
588
|
items += `
|
|
494
|
-
<li>
|
|
589
|
+
<li data-testid="confirmed-improvement-item">
|
|
495
590
|
<span class="improvement-title">${escapeHtml(imp.title)}</span>
|
|
496
591
|
<span class="confidence-tag ${confClass}">${escapeHtml(imp.confidence)}</span>
|
|
497
592
|
<div class="improvement-action">${escapeHtml(imp.action)}</div>
|
|
498
593
|
</li>`;
|
|
499
594
|
}
|
|
500
|
-
return ` <
|
|
595
|
+
return ` <section data-testid="confirmed-improvements">
|
|
596
|
+
<h2>Confirmed Improvements</h2>
|
|
501
597
|
<p>These improvements have been confirmed and will be applied. No action needed from you.</p>
|
|
502
598
|
<ul class="improvements-list">${items}
|
|
503
|
-
</ul
|
|
599
|
+
</ul>
|
|
600
|
+
</section>`;
|
|
504
601
|
}
|
|
505
602
|
// ---------------------------------------------------------------------------
|
|
506
603
|
// Embedded script
|
|
@@ -508,92 +605,102 @@ function renderImprovements(improvements) {
|
|
|
508
605
|
function renderScript(data) {
|
|
509
606
|
return ` <script>
|
|
510
607
|
(function() {
|
|
511
|
-
var submitBtn
|
|
512
|
-
var copyButtons
|
|
513
|
-
var formContainer
|
|
514
|
-
var postSubmitContainer
|
|
515
|
-
var jsonOutput
|
|
516
|
-
|
|
517
|
-
submitBtn.addEventListener("click", function() {
|
|
518
|
-
var cards = document.querySelectorAll(".card[data-item-id]");
|
|
519
|
-
var valid = true;
|
|
520
|
-
|
|
521
|
-
// Validate: every card must have a selection, and "None of these" requires a comment.
|
|
522
|
-
cards.forEach(function(card) {
|
|
523
|
-
var itemId = card.getAttribute("data-item-id");
|
|
524
|
-
var selected = card.querySelector('input[type="radio"]:checked');
|
|
525
|
-
var textarea = document.getElementById("comment-" + itemId);
|
|
526
|
-
var existingMsg = card.querySelector(".validation-msg");
|
|
527
|
-
if (existingMsg) existingMsg.remove();
|
|
528
|
-
textarea.classList.remove("validation-error");
|
|
529
|
-
|
|
530
|
-
if (!selected) {
|
|
531
|
-
var radioGroup = card.querySelector(".radio-group");
|
|
532
|
-
var msg = document.createElement("div");
|
|
533
|
-
msg.className = "validation-msg";
|
|
534
|
-
msg.textContent = "Please select an option.";
|
|
535
|
-
radioGroup.appendChild(msg);
|
|
536
|
-
valid = false;
|
|
537
|
-
return;
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
if (selected.value === "none" && !textarea.value.trim()) {
|
|
541
|
-
textarea.classList.add("validation-error");
|
|
542
|
-
var msg = document.createElement("div");
|
|
543
|
-
msg.className = "validation-msg";
|
|
544
|
-
msg.textContent = "A comment is required when selecting None of these.";
|
|
545
|
-
textarea.parentNode.appendChild(msg);
|
|
546
|
-
valid = false;
|
|
547
|
-
}
|
|
548
|
-
});
|
|
608
|
+
var submitBtn;
|
|
609
|
+
var copyButtons;
|
|
610
|
+
var formContainer;
|
|
611
|
+
var postSubmitContainer;
|
|
612
|
+
var jsonOutput;
|
|
549
613
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
var selected = card.querySelector('input[type="radio"]:checked');
|
|
557
|
-
var textarea = document.getElementById("comment-" + itemId);
|
|
558
|
-
var source = card.getAttribute("data-source") || "";
|
|
559
|
-
var labels = JSON.parse(card.getAttribute("data-labels") || "[]");
|
|
560
|
-
var chosenLabel = "";
|
|
561
|
-
if (selected.value === "none") {
|
|
562
|
-
chosenLabel = "None of these";
|
|
563
|
-
} else {
|
|
564
|
-
var idx = parseInt(selected.value.replace("opt-", ""), 10);
|
|
565
|
-
chosenLabel = labels[idx] || "";
|
|
566
|
-
}
|
|
567
|
-
decisions[itemId] = {
|
|
568
|
-
choice: selected.value,
|
|
569
|
-
chosen_label: chosenLabel,
|
|
570
|
-
comment: textarea ? textarea.value : "",
|
|
571
|
-
source: source
|
|
572
|
-
};
|
|
573
|
-
});
|
|
614
|
+
function init() {
|
|
615
|
+
submitBtn = document.getElementById("submit-btn");
|
|
616
|
+
copyButtons = Array.prototype.slice.call(document.querySelectorAll("[data-copy-json-btn]"));
|
|
617
|
+
formContainer = document.getElementById("form-container");
|
|
618
|
+
postSubmitContainer = document.getElementById("post-submit-container");
|
|
619
|
+
jsonOutput = document.getElementById("json-output");
|
|
574
620
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
general_comment: document.getElementById("general-comment").value
|
|
579
|
-
};
|
|
621
|
+
submitBtn.addEventListener("click", function() {
|
|
622
|
+
var cards = document.querySelectorAll(".card[data-item-id]");
|
|
623
|
+
var valid = true;
|
|
580
624
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
625
|
+
// Validate: every card must have a selection, and "None of these" requires a comment.
|
|
626
|
+
cards.forEach(function(card) {
|
|
627
|
+
var itemId = card.getAttribute("data-item-id");
|
|
628
|
+
var selected = card.querySelector('input[type="radio"]:checked');
|
|
629
|
+
var textarea = document.getElementById("comment-" + itemId);
|
|
630
|
+
var existingMsg = card.querySelector(".validation-msg");
|
|
631
|
+
if (existingMsg) existingMsg.remove();
|
|
632
|
+
textarea.classList.remove("validation-error");
|
|
585
633
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
634
|
+
if (!selected) {
|
|
635
|
+
var radioGroup = card.querySelector(".radio-group");
|
|
636
|
+
var msg = document.createElement("div");
|
|
637
|
+
msg.className = "validation-msg";
|
|
638
|
+
msg.textContent = "Please select an option.";
|
|
639
|
+
radioGroup.appendChild(msg);
|
|
640
|
+
valid = false;
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
if (selected.value === "none" && !textarea.value.trim()) {
|
|
645
|
+
textarea.classList.add("validation-error");
|
|
646
|
+
var msg = document.createElement("div");
|
|
647
|
+
msg.className = "validation-msg";
|
|
648
|
+
msg.textContent = "A comment is required when selecting None of these.";
|
|
649
|
+
textarea.parentNode.appendChild(msg);
|
|
650
|
+
valid = false;
|
|
651
|
+
}
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
if (!valid) return;
|
|
655
|
+
|
|
656
|
+
// Build output JSON
|
|
657
|
+
var decisions = {};
|
|
658
|
+
cards.forEach(function(card) {
|
|
659
|
+
var itemId = card.getAttribute("data-item-id");
|
|
660
|
+
var selected = card.querySelector('input[type="radio"]:checked');
|
|
661
|
+
var textarea = document.getElementById("comment-" + itemId);
|
|
662
|
+
var source = card.getAttribute("data-source") || "";
|
|
663
|
+
var labels = JSON.parse(card.getAttribute("data-labels") || "[]");
|
|
664
|
+
var chosenLabel = "";
|
|
665
|
+
if (selected.value === "none") {
|
|
666
|
+
chosenLabel = "None of these";
|
|
667
|
+
} else if (selected.value === "ask") {
|
|
668
|
+
chosenLabel = "Ask about this";
|
|
669
|
+
} else {
|
|
670
|
+
var idx = parseInt(selected.value.replace("opt-", ""), 10);
|
|
671
|
+
chosenLabel = labels[idx] || "";
|
|
672
|
+
}
|
|
673
|
+
decisions[itemId] = {
|
|
674
|
+
choice: selected.value,
|
|
675
|
+
chosen_label: chosenLabel,
|
|
676
|
+
comment: textarea ? textarea.value : "",
|
|
677
|
+
source: source
|
|
678
|
+
};
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
var output = {
|
|
682
|
+
ticket_key: ${safeJsonForScript(data.ticket_key)},
|
|
683
|
+
decisions: decisions,
|
|
684
|
+
general_comment: document.getElementById("general-comment").value
|
|
685
|
+
};
|
|
686
|
+
|
|
687
|
+
jsonOutput.textContent = JSON.stringify(output, null, 2);
|
|
688
|
+
formContainer.classList.add("hidden");
|
|
689
|
+
postSubmitContainer.classList.remove("hidden");
|
|
589
690
|
});
|
|
590
|
-
|
|
691
|
+
|
|
692
|
+
copyButtons.forEach(function(copyBtn) {
|
|
693
|
+
copyBtn.addEventListener("click", function() {
|
|
694
|
+
copyJson();
|
|
695
|
+
});
|
|
696
|
+
});
|
|
697
|
+
}
|
|
591
698
|
|
|
592
699
|
function copyJson() {
|
|
593
700
|
var text = jsonOutput.textContent;
|
|
594
701
|
try {
|
|
595
702
|
navigator.clipboard.writeText(text).then(function() {
|
|
596
|
-
setCopyButtonLabel(
|
|
703
|
+
setCopyButtonLabel(${safeJsonForScript(COPY_SUCCESS_LABEL)});
|
|
597
704
|
setTimeout(resetCopyButtonLabels, 2000);
|
|
598
705
|
}).catch(function() {
|
|
599
706
|
selectAndPrompt();
|
|
@@ -621,7 +728,13 @@ function renderScript(data) {
|
|
|
621
728
|
var sel = window.getSelection();
|
|
622
729
|
sel.removeAllRanges();
|
|
623
730
|
sel.addRange(range);
|
|
624
|
-
setCopyButtonLabel(
|
|
731
|
+
setCopyButtonLabel(${safeJsonForScript(COPY_FALLBACK_PROMPT_LABEL)});
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
if (document.readyState !== "loading") {
|
|
735
|
+
init();
|
|
736
|
+
} else {
|
|
737
|
+
document.addEventListener("DOMContentLoaded", init);
|
|
625
738
|
}
|
|
626
739
|
})();
|
|
627
740
|
</script>`;
|
package/build/index.js
CHANGED
|
@@ -24,6 +24,7 @@ import { checkForUpdate } from "./update-check.js";
|
|
|
24
24
|
import { reconstructAgentMarkdown, translateAgentToCopilot } from "./agent-utils.js";
|
|
25
25
|
import { resolveRecipe, loadCustomPipelines } from "./pipeline-utils.js";
|
|
26
26
|
import { generateDecisionPageHtml } from "./decision-page-template.js";
|
|
27
|
+
import { DecisionPageInputShape } from "./decision-page-schema.js";
|
|
27
28
|
// Mutable pipeline/instruction state — starts with bundled, merged with user at startup
|
|
28
29
|
const PIPELINES = { ...BUNDLED_PIPELINES };
|
|
29
30
|
const INSTRUCTIONS = { ...BUNDLED_INSTRUCTIONS };
|
|
@@ -2290,36 +2291,15 @@ server.registerTool("get_pipeline_recipe", {
|
|
|
2290
2291
|
// ---------------------------------------------------------------------------
|
|
2291
2292
|
server.registerTool("generate_decision_page", {
|
|
2292
2293
|
description: "Generate a local HTML decision page for capturing user decisions on ticket review findings. " +
|
|
2293
|
-
"Renders recommendation-driven review decisions
|
|
2294
|
-
"
|
|
2295
|
-
"in a browser, makes selections, and
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
actionable_items: z
|
|
2299
|
-
.array(z.object({
|
|
2300
|
-
id: z.string().min(1),
|
|
2301
|
-
question: z.string().min(1),
|
|
2302
|
-
context: z.string().min(1),
|
|
2303
|
-
source: z.string().min(1).describe("Source reference from the evaluation, e.g. 'Clarifying Q3 (initial round)'"),
|
|
2304
|
-
recommendation_index: z.number().int().min(0).describe("0-based index of the recommended option in the options array"),
|
|
2305
|
-
options: z.array(z.string().min(1)).min(1).describe("Option labels from resolution guide decision tree branches. Values are auto-generated."),
|
|
2306
|
-
}))
|
|
2307
|
-
.optional()
|
|
2308
|
-
.default([])
|
|
2309
|
-
.describe("Actionable review decisions with option labels from resolution guide decision trees. 'None of these' auto-appended."),
|
|
2310
|
-
clear_improvements: z
|
|
2311
|
-
.array(z.object({
|
|
2312
|
-
id: z.string().min(1),
|
|
2313
|
-
title: z.string().min(1),
|
|
2314
|
-
action: z.string().min(1),
|
|
2315
|
-
confidence: z.string().min(1),
|
|
2316
|
-
source: z.string().min(1).describe("Source reference from the evaluation"),
|
|
2317
|
-
}))
|
|
2318
|
-
.optional()
|
|
2319
|
-
.default([])
|
|
2320
|
-
.describe("Confirmed improvements displayed as informational list, not submitted."),
|
|
2321
|
-
},
|
|
2294
|
+
"Renders recommendation-driven review decisions sourced from the combined review-and-resolution " +
|
|
2295
|
+
"document, with per-option consequence lines, a closed-by-default codebase-evidence disclosure, " +
|
|
2296
|
+
"and confirmed improvements. The user opens the HTML file in a browser, makes selections, and " +
|
|
2297
|
+
"copies the resulting JSON output back to the agent.",
|
|
2298
|
+
inputSchema: DecisionPageInputShape,
|
|
2322
2299
|
}, async (input) => {
|
|
2300
|
+
// Returned messages travel through JSON.stringify in the MCP envelope below,
|
|
2301
|
+
// never into HTML, so echoing untrusted input back to the caller is safe here.
|
|
2302
|
+
// Do not reuse this helper in any path that renders the message into HTML.
|
|
2323
2303
|
const validationError = (message) => ({
|
|
2324
2304
|
content: [{
|
|
2325
2305
|
type: "text",
|
|
@@ -2343,21 +2323,27 @@ server.registerTool("generate_decision_page", {
|
|
|
2343
2323
|
}],
|
|
2344
2324
|
};
|
|
2345
2325
|
}
|
|
2346
|
-
// Validate actionable_items
|
|
2326
|
+
// Validate actionable_items: cross-item invariants Zod cannot express.
|
|
2327
|
+
// Per-item bounds (recommendation_index < options.length, parity, branch
|
|
2328
|
+
// count, etc.) are enforced by the schema's superRefine.
|
|
2347
2329
|
const seenIds = new Set();
|
|
2348
2330
|
for (const item of input.actionable_items) {
|
|
2349
2331
|
if (seenIds.has(item.id)) {
|
|
2350
2332
|
return validationError(`Duplicate actionable_items id: "${item.id}"`);
|
|
2351
2333
|
}
|
|
2352
2334
|
seenIds.add(item.id);
|
|
2353
|
-
if (item.recommendation_index >= item.options.length) {
|
|
2354
|
-
return validationError(`Item "${item.id}": recommendation_index ${item.recommendation_index} is out of bounds (${item.options.length} options).`);
|
|
2355
|
-
}
|
|
2356
2335
|
const noneLabel = item.options.find((label) => label.toLowerCase() === "none of these");
|
|
2357
2336
|
if (noneLabel) {
|
|
2358
2337
|
return validationError(`Item "${item.id}": option label "${noneLabel}" is reserved and auto-appended by the tool.`);
|
|
2359
2338
|
}
|
|
2360
2339
|
}
|
|
2340
|
+
const seenCiIds = new Set();
|
|
2341
|
+
for (const ci of input.clear_improvements) {
|
|
2342
|
+
if (seenCiIds.has(ci.id)) {
|
|
2343
|
+
return validationError(`Duplicate clear_improvements id: "${ci.id}"`);
|
|
2344
|
+
}
|
|
2345
|
+
seenCiIds.add(ci.id);
|
|
2346
|
+
}
|
|
2361
2347
|
// Read design assets and base64-encode for embedding
|
|
2362
2348
|
const assetsDir = path.join(PROJECT_ROOT, "design-assets");
|
|
2363
2349
|
const fontsDir = path.join(PROJECT_ROOT, "public", "fonts");
|