mrvn-cli 0.5.3 → 0.5.5

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.
@@ -176,9 +176,9 @@ var DocumentStore = class {
176
176
  }
177
177
  }
178
178
  }
179
- list(query3) {
179
+ list(query4) {
180
180
  const results = [];
181
- const types = query3?.type ? [query3.type] : Object.keys(this.typeDirs);
181
+ const types = query4?.type ? [query4.type] : Object.keys(this.typeDirs);
182
182
  for (const type of types) {
183
183
  const dirName = this.typeDirs[type];
184
184
  if (!dirName) continue;
@@ -189,9 +189,9 @@ var DocumentStore = class {
189
189
  const filePath = path3.join(dir, file2);
190
190
  const raw = fs3.readFileSync(filePath, "utf-8");
191
191
  const doc = parseDocument(raw, filePath);
192
- if (query3?.status && doc.frontmatter.status !== query3.status) continue;
193
- if (query3?.owner && doc.frontmatter.owner !== query3.owner) continue;
194
- if (query3?.tag && (!doc.frontmatter.tags || !doc.frontmatter.tags.includes(query3.tag)))
192
+ if (query4?.status && doc.frontmatter.status !== query4.status) continue;
193
+ if (query4?.owner && doc.frontmatter.owner !== query4.owner) continue;
194
+ if (query4?.tag && (!doc.frontmatter.tags || !doc.frontmatter.tags.includes(query4.tag)))
195
195
  continue;
196
196
  results.push(doc);
197
197
  }
@@ -269,14 +269,19 @@ var DocumentStore = class {
269
269
  if (!existing) {
270
270
  throw new Error(`Document ${id} not found`);
271
271
  }
272
+ const keysToDelete = Object.entries(updates).filter(([, v]) => v === void 0).map(([k]) => k);
272
273
  const cleanedUpdates = Object.fromEntries(
273
274
  Object.entries(updates).filter(([, v]) => v !== void 0)
274
275
  );
275
- const updatedFrontmatter = {
276
+ const merged = {
276
277
  ...existing.frontmatter,
277
278
  ...cleanedUpdates,
278
279
  updated: (/* @__PURE__ */ new Date()).toISOString()
279
280
  };
281
+ for (const key of keysToDelete) {
282
+ delete merged[key];
283
+ }
284
+ const updatedFrontmatter = merged;
280
285
  const doc = {
281
286
  frontmatter: updatedFrontmatter,
282
287
  content: content ?? existing.content,
@@ -15672,7 +15677,7 @@ function collectSprintSummaryData(store, sprintId) {
15672
15677
  });
15673
15678
  const sprintTag = `sprint:${fm.id}`;
15674
15679
  const workItemDocs = allDocs.filter(
15675
- (d) => d.frontmatter.type !== "sprint" && d.frontmatter.type !== "epic" && d.frontmatter.type !== "meeting" && d.frontmatter.tags?.includes(sprintTag)
15680
+ (d) => d.frontmatter.type !== "sprint" && d.frontmatter.type !== "epic" && d.frontmatter.type !== "meeting" && d.frontmatter.type !== "decision" && d.frontmatter.type !== "question" && d.frontmatter.tags?.includes(sprintTag)
15676
15681
  );
15677
15682
  const primaryDocs = workItemDocs.filter((d) => d.frontmatter.type !== "contribution");
15678
15683
  const byStatus = {};
@@ -15810,16 +15815,13 @@ function collectSprintSummaryData(store, sprintId) {
15810
15815
  title: d.frontmatter.title,
15811
15816
  type: d.frontmatter.type
15812
15817
  }));
15813
- const riskBlockers = allDocs.filter(
15814
- (d) => !DONE_STATUSES2.has(d.frontmatter.status) && d.frontmatter.tags?.includes("risk") && d.frontmatter.tags?.some((t) => relevantTags.has(t)) && !blockers.some((b) => b.id === d.frontmatter.id)
15815
- );
15816
- for (const d of riskBlockers) {
15817
- blockers.push({
15818
- id: d.frontmatter.id,
15819
- title: d.frontmatter.title,
15820
- type: d.frontmatter.type
15821
- });
15822
- }
15818
+ const risks = allDocs.filter(
15819
+ (d) => !DONE_STATUSES2.has(d.frontmatter.status) && d.frontmatter.tags?.includes("risk") && d.frontmatter.tags?.some((t) => relevantTags.has(t))
15820
+ ).map((d) => ({
15821
+ id: d.frontmatter.id,
15822
+ title: d.frontmatter.title,
15823
+ type: d.frontmatter.type
15824
+ }));
15823
15825
  let velocity = null;
15824
15826
  const currentRate = workItems.completionPct;
15825
15827
  const completedSprints = sprintDocs.filter((s) => DONE_STATUSES2.has(s.frontmatter.status) && s.frontmatter.id !== fm.id).sort((a, b) => (b.frontmatter.endDate ?? "").localeCompare(a.frontmatter.endDate ?? ""));
@@ -15855,6 +15857,7 @@ function collectSprintSummaryData(store, sprintId) {
15855
15857
  openActions,
15856
15858
  openQuestions,
15857
15859
  blockers,
15860
+ risks,
15858
15861
  velocity
15859
15862
  };
15860
15863
  }
@@ -16440,6 +16443,13 @@ function buildPrompt(data) {
16440
16443
  sections.push(`- ${b.id} (${b.type}): ${b.title}`);
16441
16444
  }
16442
16445
  }
16446
+ if (data.risks.length > 0) {
16447
+ sections.push(`
16448
+ ## Risks`);
16449
+ for (const r of data.risks) {
16450
+ sections.push(`- ${r.id} (${r.type}): ${r.title}`);
16451
+ }
16452
+ }
16443
16453
  if (data.velocity) {
16444
16454
  sections.push(`
16445
16455
  ## Velocity`);
@@ -20078,7 +20088,7 @@ ${fragment}`);
20078
20088
  import { tool as tool24 } from "@anthropic-ai/claude-agent-sdk";
20079
20089
 
20080
20090
  // src/skills/action-runner.ts
20081
- import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
20091
+ import { query as query3 } from "@anthropic-ai/claude-agent-sdk";
20082
20092
 
20083
20093
  // src/agent/mcp-server.ts
20084
20094
  import {
@@ -20567,6 +20577,7 @@ a:hover { text-decoration: underline; }
20567
20577
  }
20568
20578
 
20569
20579
  .card a { color: inherit; text-decoration: none; display: block; }
20580
+ a.card-link { color: inherit; text-decoration: none; cursor: pointer; }
20570
20581
 
20571
20582
  .card .card-label {
20572
20583
  font-size: 0.75rem;
@@ -20982,6 +20993,68 @@ tr:hover td {
20982
20993
  .priority-medium { color: var(--amber); }
20983
20994
  .priority-low { color: var(--green); }
20984
20995
 
20996
+ /* Blocker / Risk detail cards */
20997
+ .blocker-card {
20998
+ background: var(--bg-card);
20999
+ border: 1px solid var(--border);
21000
+ border-radius: var(--radius);
21001
+ padding: 1.25rem;
21002
+ margin-bottom: 1rem;
21003
+ }
21004
+ .blocker-card-header {
21005
+ display: flex;
21006
+ align-items: center;
21007
+ gap: 0.5rem;
21008
+ font-size: 0.85rem;
21009
+ margin-bottom: 0.25rem;
21010
+ }
21011
+ .blocker-card-title {
21012
+ margin: 0.25rem 0 0.5rem;
21013
+ font-size: 1rem;
21014
+ }
21015
+ .blocker-card-meta {
21016
+ display: flex;
21017
+ flex-wrap: wrap;
21018
+ gap: 1rem;
21019
+ font-size: 0.85rem;
21020
+ color: var(--text-dim);
21021
+ margin-bottom: 0.75rem;
21022
+ }
21023
+ .blocker-card-content {
21024
+ border-top: 1px solid var(--border);
21025
+ padding-top: 0.75rem;
21026
+ font-size: 0.9rem;
21027
+ }
21028
+ .risk-assessment-content {
21029
+ background: var(--bg);
21030
+ border: 1px solid var(--border);
21031
+ border-left: 3px solid var(--amber);
21032
+ border-radius: var(--radius);
21033
+ padding: 1rem 1.25rem;
21034
+ margin-top: 0.75rem;
21035
+ font-size: 0.9rem;
21036
+ }
21037
+ .risk-assess-btn {
21038
+ font-size: 0.8rem;
21039
+ padding: 0.4rem 0.8rem;
21040
+ margin-top: 0.75rem;
21041
+ }
21042
+ .risk-assess-loading {
21043
+ margin-top: 0.75rem;
21044
+ font-size: 0.85rem;
21045
+ }
21046
+ .risk-assess-error {
21047
+ margin-top: 0.5rem;
21048
+ }
21049
+ .risk-assessment-label {
21050
+ font-size: 0.7rem;
21051
+ text-transform: uppercase;
21052
+ letter-spacing: 0.08em;
21053
+ color: var(--amber);
21054
+ font-weight: 600;
21055
+ margin-bottom: 0.5rem;
21056
+ }
21057
+
20985
21058
  /* Health */
20986
21059
  .health-section-title {
20987
21060
  font-size: 1.1rem;
@@ -22098,6 +22171,134 @@ function personaPickerPage() {
22098
22171
  </div>`;
22099
22172
  }
22100
22173
 
22174
+ // src/reports/sprint-summary/risk-assessment.ts
22175
+ import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
22176
+ var SYSTEM_PROMPT2 = `You are a delivery management assistant generating a data-driven risk assessment.
22177
+
22178
+ IMPORTANT: All the data you need is provided in the user message below. Do NOT attempt to look up, search for, or request additional information. Analyze ONLY the data given and produce your assessment immediately.
22179
+
22180
+ Produce a concise markdown assessment with these sections:
22181
+
22182
+ ## Status Assessment
22183
+ One-line verdict: is this risk actively being mitigated, stalled, or escalating?
22184
+
22185
+ ## Related Activity
22186
+ What actions, decisions, or contributions are connected to this risk? How are they progressing? Be specific \u2014 reference artifact IDs from the data provided.
22187
+
22188
+ ## Trajectory
22189
+ Based on the data (status of related items, time remaining, ownership), is this risk trending toward resolution or toward becoming a blocker? Explain your reasoning with concrete evidence.
22190
+
22191
+ ## Recommendation
22192
+ One concrete next step to move this risk toward resolution.
22193
+
22194
+ Rules:
22195
+ - Reference artifact IDs, dates, owners, and statuses from the provided data
22196
+ - Keep the tone professional and direct
22197
+ - Do NOT speculate beyond what the data supports \u2014 if information is insufficient, say so explicitly
22198
+ - Do NOT ask for more information or say you will look things up \u2014 everything you need is in the prompt
22199
+ - Produce the full assessment text directly`;
22200
+ async function generateRiskAssessment(data, riskId, store) {
22201
+ const risk = data.risks.find((r) => r.id === riskId);
22202
+ if (!risk) return "Risk not found in sprint data.";
22203
+ const prompt = buildSingleRiskPrompt(data, risk, store);
22204
+ const result = query2({
22205
+ prompt,
22206
+ options: {
22207
+ systemPrompt: SYSTEM_PROMPT2,
22208
+ maxTurns: 1,
22209
+ tools: [],
22210
+ allowedTools: []
22211
+ }
22212
+ });
22213
+ for await (const msg of result) {
22214
+ if (msg.type === "assistant") {
22215
+ const text = msg.message.content.find(
22216
+ (b) => b.type === "text"
22217
+ );
22218
+ if (text) return text.text;
22219
+ }
22220
+ }
22221
+ return "Unable to generate risk assessment.";
22222
+ }
22223
+ function buildSingleRiskPrompt(data, risk, store) {
22224
+ const sections = [];
22225
+ sections.push(`# Sprint: ${data.sprint.id} \u2014 ${data.sprint.title}`);
22226
+ if (data.sprint.goal) sections.push(`Goal: ${data.sprint.goal}`);
22227
+ sections.push(`Days remaining: ${data.timeline.daysRemaining} / ${data.timeline.totalDays}`);
22228
+ sections.push(`Completion: ${data.workItems.completionPct}%`);
22229
+ sections.push("");
22230
+ const doc = store.get(risk.id);
22231
+ sections.push(`# RISK: ${risk.id} \u2014 ${risk.title}`);
22232
+ sections.push(`Type: ${risk.type}`);
22233
+ if (doc) {
22234
+ sections.push(`Status: ${doc.frontmatter.status}`);
22235
+ if (doc.frontmatter.owner) sections.push(`Owner: ${doc.frontmatter.owner}`);
22236
+ if (doc.frontmatter.assignee) sections.push(`Assignee: ${doc.frontmatter.assignee}`);
22237
+ if (doc.frontmatter.priority) sections.push(`Priority: ${doc.frontmatter.priority}`);
22238
+ if (doc.frontmatter.dueDate) sections.push(`Due date: ${doc.frontmatter.dueDate}`);
22239
+ if (doc.frontmatter.created) sections.push(`Created: ${doc.frontmatter.created.slice(0, 10)}`);
22240
+ const tags = doc.frontmatter.tags ?? [];
22241
+ if (tags.length > 0) sections.push(`Tags: ${tags.join(", ")}`);
22242
+ if (doc.content.trim()) {
22243
+ sections.push(`
22244
+ Description:
22245
+ ${doc.content.trim()}`);
22246
+ }
22247
+ const allDocs = store.list();
22248
+ const relatedIds = /* @__PURE__ */ new Set();
22249
+ for (const d of allDocs) {
22250
+ if (d.frontmatter.aboutArtifact === risk.id) {
22251
+ relatedIds.add(d.frontmatter.id);
22252
+ }
22253
+ }
22254
+ const idPattern = /\b([A-Z]-\d{3,})\b/g;
22255
+ let match;
22256
+ while ((match = idPattern.exec(doc.content)) !== null) {
22257
+ relatedIds.add(match[1]);
22258
+ }
22259
+ const significantTags = tags.filter(
22260
+ (t) => !t.startsWith("sprint:") && !t.startsWith("focus:") && t !== "risk"
22261
+ );
22262
+ if (significantTags.length > 0) {
22263
+ for (const d of allDocs) {
22264
+ if (d.frontmatter.id === risk.id) continue;
22265
+ const dTags = d.frontmatter.tags ?? [];
22266
+ if (significantTags.some((t) => dTags.includes(t))) {
22267
+ relatedIds.add(d.frontmatter.id);
22268
+ }
22269
+ }
22270
+ }
22271
+ const about = doc.frontmatter.aboutArtifact;
22272
+ if (about) {
22273
+ relatedIds.add(about);
22274
+ for (const d of allDocs) {
22275
+ if (d.frontmatter.aboutArtifact === about && d.frontmatter.id !== risk.id) {
22276
+ relatedIds.add(d.frontmatter.id);
22277
+ }
22278
+ }
22279
+ }
22280
+ const relatedDocs = [...relatedIds].map((id) => store.get(id)).filter((d) => d != null).slice(0, 20);
22281
+ if (relatedDocs.length > 0) {
22282
+ sections.push(`
22283
+ ## Related Documents (${relatedDocs.length})`);
22284
+ for (const rd of relatedDocs) {
22285
+ const owner = rd.frontmatter.owner ?? "unowned";
22286
+ const summary = rd.content.trim().slice(0, 300);
22287
+ sections.push(
22288
+ `- ${rd.frontmatter.id} (${rd.frontmatter.type}) [${rd.frontmatter.status}] \u2014 ${rd.frontmatter.title}`
22289
+ );
22290
+ sections.push(` Owner: ${owner}${rd.frontmatter.dueDate ? `, Due: ${rd.frontmatter.dueDate}` : ""}`);
22291
+ if (summary) sections.push(` Summary: ${summary}${rd.content.trim().length > 300 ? "..." : ""}`);
22292
+ }
22293
+ }
22294
+ }
22295
+ sections.push("");
22296
+ sections.push(`---`);
22297
+ sections.push(`
22298
+ Generate the risk assessment for ${risk.id} based on the data above.`);
22299
+ return sections.join("\n");
22300
+ }
22301
+
22101
22302
  // src/personas/builtin/product-owner.ts
22102
22303
  var productOwner = {
22103
22304
  id: "product-owner",
@@ -23495,11 +23696,16 @@ function sprintSummaryPage(data, cached2) {
23495
23696
  <div class="card-value">${data.linkedEpics.length}</div>
23496
23697
  <div class="card-sub">linked to sprint</div>
23497
23698
  </div>
23498
- <div class="card">
23699
+ <a class="card card-link" href="sprint-blockers">
23499
23700
  <div class="card-label">Blockers</div>
23500
23701
  <div class="card-value${data.blockers.length > 0 ? " priority-high" : ""}">${data.blockers.length}</div>
23501
- <div class="card-sub">${data.openActions.length} open actions</div>
23502
- </div>
23702
+ <div class="card-sub">${data.workItems.blocked} blocked items</div>
23703
+ </a>
23704
+ <a class="card card-link" href="sprint-risks">
23705
+ <div class="card-label">Risks</div>
23706
+ <div class="card-value${data.risks.length > 0 ? " priority-medium" : ""}">${data.risks.length}</div>
23707
+ <div class="card-sub">open risk items</div>
23708
+ </a>
23503
23709
  </div>`;
23504
23710
  const epicsTable = data.linkedEpics.length > 0 ? collapsibleSection(
23505
23711
  "ss-epics",
@@ -24889,6 +25095,168 @@ function upcomingPage(data) {
24889
25095
  `;
24890
25096
  }
24891
25097
 
25098
+ // src/web/templates/pages/sprint-blockers.ts
25099
+ function sprintBlockersPage(data, store) {
25100
+ if (!data) {
25101
+ return `
25102
+ <div class="page-header">
25103
+ <h2>Sprint Blockers</h2>
25104
+ <div class="subtitle">Blocked items in the active sprint</div>
25105
+ </div>
25106
+ <div class="empty">
25107
+ <h3>No Active Sprint</h3>
25108
+ <p>No active sprint found.</p>
25109
+ </div>`;
25110
+ }
25111
+ const blockerDocs = data.blockers.map((b) => {
25112
+ const doc = store.get(b.id);
25113
+ return { ...b, doc };
25114
+ });
25115
+ const statsCards = `
25116
+ <div class="cards">
25117
+ <div class="card">
25118
+ <div class="card-label">Blocked Items</div>
25119
+ <div class="card-value${blockerDocs.length > 0 ? " priority-high" : ""}">${blockerDocs.length}</div>
25120
+ <div class="card-sub">in ${escapeHtml(data.sprint.id)}</div>
25121
+ </div>
25122
+ </div>`;
25123
+ const itemCards = blockerDocs.map((b) => {
25124
+ const doc = b.doc;
25125
+ const owner = doc?.frontmatter.owner;
25126
+ const assignee = doc?.frontmatter.assignee;
25127
+ const content = doc?.content?.trim();
25128
+ return `
25129
+ <div class="blocker-card">
25130
+ <div class="blocker-card-header">
25131
+ <a href="/docs/${escapeHtml(b.type)}/${escapeHtml(b.id)}">${escapeHtml(b.id)}</a>
25132
+ <span class="text-dim">${escapeHtml(typeLabel(b.type))}</span>
25133
+ ${statusBadge("blocked")}
25134
+ </div>
25135
+ <h4 class="blocker-card-title">${escapeHtml(b.title)}</h4>
25136
+ <div class="blocker-card-meta">
25137
+ ${owner ? `<span><strong>Owner:</strong> ${escapeHtml(owner)}</span>` : ""}
25138
+ ${assignee ? `<span><strong>Assignee:</strong> ${escapeHtml(assignee)}</span>` : ""}
25139
+ ${doc?.frontmatter.created ? `<span><strong>Created:</strong> ${formatDate(doc.frontmatter.created)}</span>` : ""}
25140
+ </div>
25141
+ ${content ? `<div class="blocker-card-content detail-content">${renderMarkdown(content)}</div>` : ""}
25142
+ </div>`;
25143
+ }).join("");
25144
+ const emptyMessage = blockerDocs.length === 0 ? `<div class="empty"><h3>No Blockers</h3><p>No blocked items in this sprint.</p></div>` : "";
25145
+ return `
25146
+ <div class="page-header">
25147
+ <h2>Sprint Blockers</h2>
25148
+ <div class="subtitle">Blocked items in ${escapeHtml(data.sprint.id)} \u2014 ${escapeHtml(data.sprint.title)}</div>
25149
+ </div>
25150
+ ${statsCards}
25151
+ ${emptyMessage}
25152
+ ${itemCards}`;
25153
+ }
25154
+
25155
+ // src/web/templates/pages/sprint-risks.ts
25156
+ function sprintRisksPage(data, store) {
25157
+ if (!data) {
25158
+ return `
25159
+ <div class="page-header">
25160
+ <h2>Sprint Risks</h2>
25161
+ <div class="subtitle">Risk items in the active sprint</div>
25162
+ </div>
25163
+ <div class="empty">
25164
+ <h3>No Active Sprint</h3>
25165
+ <p>No active sprint found.</p>
25166
+ </div>`;
25167
+ }
25168
+ const riskDocs = data.risks.map((r) => {
25169
+ const doc = store.get(r.id);
25170
+ return { ...r, doc };
25171
+ });
25172
+ const statsCards = `
25173
+ <div class="cards">
25174
+ <div class="card">
25175
+ <div class="card-label">Open Risks</div>
25176
+ <div class="card-value${riskDocs.length > 0 ? " priority-medium" : ""}">${riskDocs.length}</div>
25177
+ <div class="card-sub">in ${escapeHtml(data.sprint.id)}</div>
25178
+ </div>
25179
+ </div>`;
25180
+ const itemCards = riskDocs.map((r) => {
25181
+ const doc = r.doc;
25182
+ const owner = doc?.frontmatter.owner;
25183
+ const assignee = doc?.frontmatter.assignee;
25184
+ const content = doc?.content?.trim();
25185
+ return `
25186
+ <div class="blocker-card" id="risk-${escapeHtml(r.id)}">
25187
+ <div class="blocker-card-header">
25188
+ <a href="/docs/${escapeHtml(r.type)}/${escapeHtml(r.id)}">${escapeHtml(r.id)}</a>
25189
+ <span class="text-dim">${escapeHtml(typeLabel(r.type))}</span>
25190
+ ${statusBadge(doc?.frontmatter.status ?? "open")}
25191
+ </div>
25192
+ <h4 class="blocker-card-title">${escapeHtml(r.title)}</h4>
25193
+ <div class="blocker-card-meta">
25194
+ ${owner ? `<span><strong>Owner:</strong> ${escapeHtml(owner)}</span>` : ""}
25195
+ ${assignee ? `<span><strong>Assignee:</strong> ${escapeHtml(assignee)}</span>` : ""}
25196
+ ${doc?.frontmatter.created ? `<span><strong>Created:</strong> ${formatDate(doc.frontmatter.created)}</span>` : ""}
25197
+ </div>
25198
+ ${content ? `<div class="blocker-card-content detail-content">${renderMarkdown(content)}</div>` : ""}
25199
+ <div class="risk-assessment" id="assessment-${escapeHtml(r.id)}">
25200
+ <button class="sprint-generate-btn risk-assess-btn" onclick="generateAssessment('${escapeHtml(r.id)}', this)">Assess Risk</button>
25201
+ <div class="sprint-loading risk-assess-loading" style="display:none">
25202
+ <div class="sprint-spinner"></div>
25203
+ <span>Analyzing...</span>
25204
+ </div>
25205
+ <div class="sprint-error risk-assess-error" style="display:none"></div>
25206
+ <div class="risk-assessment-content detail-content" style="display:none"></div>
25207
+ </div>
25208
+ </div>`;
25209
+ }).join("");
25210
+ const emptyMessage = riskDocs.length === 0 ? `<div class="empty"><h3>No Risks</h3><p>No open risk items in this sprint.</p></div>` : "";
25211
+ const script = riskDocs.length > 0 ? `
25212
+ <script>
25213
+ async function generateAssessment(riskId, btn) {
25214
+ var container = document.getElementById('assessment-' + riskId);
25215
+ var loading = container.querySelector('.risk-assess-loading');
25216
+ var errorEl = container.querySelector('.risk-assess-error');
25217
+ var contentEl = container.querySelector('.risk-assessment-content');
25218
+
25219
+ btn.disabled = true;
25220
+ btn.style.display = 'none';
25221
+ loading.style.display = 'flex';
25222
+ errorEl.style.display = 'none';
25223
+
25224
+ try {
25225
+ var res = await fetch('/api/risk-assessment', {
25226
+ method: 'POST',
25227
+ headers: { 'Content-Type': 'application/json' },
25228
+ body: JSON.stringify({ sprintId: '${escapeHtml(data.sprint.id)}', riskId: riskId })
25229
+ });
25230
+ var json = await res.json();
25231
+ if (!res.ok) throw new Error(json.error || 'Failed to generate assessment');
25232
+
25233
+ contentEl.innerHTML = '<div class="risk-assessment-label">AI Assessment</div>' + json.html;
25234
+ contentEl.style.display = 'block';
25235
+
25236
+ loading.style.display = 'none';
25237
+ btn.textContent = 'Regenerate';
25238
+ btn.style.display = '';
25239
+ btn.disabled = false;
25240
+ } catch (e) {
25241
+ loading.style.display = 'none';
25242
+ errorEl.textContent = e.message;
25243
+ errorEl.style.display = 'block';
25244
+ btn.style.display = '';
25245
+ btn.disabled = false;
25246
+ }
25247
+ }
25248
+ </script>` : "";
25249
+ return `
25250
+ <div class="page-header">
25251
+ <h2>Sprint Risks</h2>
25252
+ <div class="subtitle">Risk items in ${escapeHtml(data.sprint.id)} \u2014 ${escapeHtml(data.sprint.title)}</div>
25253
+ </div>
25254
+ ${statsCards}
25255
+ ${emptyMessage}
25256
+ ${itemCards}
25257
+ ${script}`;
25258
+ }
25259
+
24892
25260
  // src/web/templates/pages/shared-wrappers.ts
24893
25261
  function sharedTimelinePage(ctx) {
24894
25262
  const diagrams = getDiagramData(ctx.store);
@@ -24918,6 +25286,16 @@ function sharedSprintSummaryPage(ctx) {
24918
25286
  const data = getSprintSummaryData(ctx.store, sprintId);
24919
25287
  return sprintSummaryPage(data);
24920
25288
  }
25289
+ function sharedSprintBlockersPage(ctx) {
25290
+ const sprintId = ctx.searchParams?.get("sprint") ?? void 0;
25291
+ const data = getSprintSummaryData(ctx.store, sprintId);
25292
+ return sprintBlockersPage(data, ctx.store);
25293
+ }
25294
+ function sharedSprintRisksPage(ctx) {
25295
+ const sprintId = ctx.searchParams?.get("sprint") ?? void 0;
25296
+ const data = getSprintSummaryData(ctx.store, sprintId);
25297
+ return sprintRisksPage(data, ctx.store);
25298
+ }
24921
25299
 
24922
25300
  // src/web/shared-page-registration.ts
24923
25301
  var SHARED_PAGES = [
@@ -24926,7 +25304,9 @@ var SHARED_PAGES = [
24926
25304
  { pageId: "upcoming", renderer: sharedUpcomingPage },
24927
25305
  { pageId: "gar", renderer: sharedGarPage },
24928
25306
  { pageId: "health", renderer: sharedHealthPage },
24929
- { pageId: "sprint-summary", renderer: sharedSprintSummaryPage }
25307
+ { pageId: "sprint-summary", renderer: sharedSprintSummaryPage },
25308
+ { pageId: "sprint-blockers", renderer: sharedSprintBlockersPage },
25309
+ { pageId: "sprint-risks", renderer: sharedSprintRisksPage }
24930
25310
  ];
24931
25311
  for (const persona of ["po", "dm", "tl"]) {
24932
25312
  for (const { pageId, renderer } of SHARED_PAGES) {
@@ -25127,6 +25507,37 @@ function handleRequest(req, res, store, projectName, navGroups) {
25127
25507
  });
25128
25508
  return;
25129
25509
  }
25510
+ if (pathname === "/api/risk-assessment" && req.method === "POST") {
25511
+ let bodyStr = "";
25512
+ req.on("data", (chunk) => {
25513
+ bodyStr += chunk;
25514
+ });
25515
+ req.on("end", async () => {
25516
+ try {
25517
+ const { sprintId, riskId } = JSON.parse(bodyStr || "{}");
25518
+ if (!riskId) {
25519
+ res.writeHead(400, { "Content-Type": "application/json" });
25520
+ res.end(JSON.stringify({ error: "riskId is required" }));
25521
+ return;
25522
+ }
25523
+ const data = getSprintSummaryData(store, sprintId);
25524
+ if (!data) {
25525
+ res.writeHead(404, { "Content-Type": "application/json" });
25526
+ res.end(JSON.stringify({ error: "Sprint not found" }));
25527
+ return;
25528
+ }
25529
+ const markdown = await generateRiskAssessment(data, riskId, store);
25530
+ const html = renderMarkdown(markdown);
25531
+ res.writeHead(200, { "Content-Type": "application/json" });
25532
+ res.end(JSON.stringify({ riskId, html }));
25533
+ } catch (err) {
25534
+ console.error("[marvin web] Risk assessment generation error:", err);
25535
+ res.writeHead(500, { "Content-Type": "application/json" });
25536
+ res.end(JSON.stringify({ error: "Failed to generate risk assessment" }));
25537
+ }
25538
+ });
25539
+ return;
25540
+ }
25130
25541
  const detailMatch = pathname.match(/^\/docs\/([^/]+)\/([^/]+)$/);
25131
25542
  if (detailMatch) {
25132
25543
  const [, type, id] = detailMatch;
@@ -25978,7 +26389,7 @@ async function runSkillAction(action, userPrompt, context) {
25978
26389
  try {
25979
26390
  const mcpServer = createMarvinMcpServer(context.store);
25980
26391
  const allowedTools = action.allowGovernanceTools !== false ? GOVERNANCE_TOOL_NAMES : [];
25981
- const conversation = query2({
26392
+ const conversation = query3({
25982
26393
  prompt: userPrompt,
25983
26394
  options: {
25984
26395
  systemPrompt: action.systemPrompt,