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.
package/dist/index.js CHANGED
@@ -223,9 +223,9 @@ var DocumentStore = class {
223
223
  }
224
224
  }
225
225
  }
226
- list(query8) {
226
+ list(query9) {
227
227
  const results = [];
228
- const types = query8?.type ? [query8.type] : Object.keys(this.typeDirs);
228
+ const types = query9?.type ? [query9.type] : Object.keys(this.typeDirs);
229
229
  for (const type of types) {
230
230
  const dirName = this.typeDirs[type];
231
231
  if (!dirName) continue;
@@ -236,9 +236,9 @@ var DocumentStore = class {
236
236
  const filePath = path3.join(dir, file2);
237
237
  const raw = fs3.readFileSync(filePath, "utf-8");
238
238
  const doc = parseDocument(raw, filePath);
239
- if (query8?.status && doc.frontmatter.status !== query8.status) continue;
240
- if (query8?.owner && doc.frontmatter.owner !== query8.owner) continue;
241
- if (query8?.tag && (!doc.frontmatter.tags || !doc.frontmatter.tags.includes(query8.tag)))
239
+ if (query9?.status && doc.frontmatter.status !== query9.status) continue;
240
+ if (query9?.owner && doc.frontmatter.owner !== query9.owner) continue;
241
+ if (query9?.tag && (!doc.frontmatter.tags || !doc.frontmatter.tags.includes(query9.tag)))
242
242
  continue;
243
243
  results.push(doc);
244
244
  }
@@ -316,14 +316,19 @@ var DocumentStore = class {
316
316
  if (!existing) {
317
317
  throw new Error(`Document ${id} not found`);
318
318
  }
319
+ const keysToDelete = Object.entries(updates).filter(([, v]) => v === void 0).map(([k]) => k);
319
320
  const cleanedUpdates = Object.fromEntries(
320
321
  Object.entries(updates).filter(([, v]) => v !== void 0)
321
322
  );
322
- const updatedFrontmatter = {
323
+ const merged = {
323
324
  ...existing.frontmatter,
324
325
  ...cleanedUpdates,
325
326
  updated: (/* @__PURE__ */ new Date()).toISOString()
326
327
  };
328
+ for (const key of keysToDelete) {
329
+ delete merged[key];
330
+ }
331
+ const updatedFrontmatter = merged;
327
332
  const doc = {
328
333
  frontmatter: updatedFrontmatter,
329
334
  content: content ?? existing.content,
@@ -15736,7 +15741,7 @@ function collectSprintSummaryData(store, sprintId) {
15736
15741
  });
15737
15742
  const sprintTag = `sprint:${fm.id}`;
15738
15743
  const workItemDocs = allDocs.filter(
15739
- (d) => d.frontmatter.type !== "sprint" && d.frontmatter.type !== "epic" && d.frontmatter.type !== "meeting" && d.frontmatter.tags?.includes(sprintTag)
15744
+ (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)
15740
15745
  );
15741
15746
  const primaryDocs = workItemDocs.filter((d) => d.frontmatter.type !== "contribution");
15742
15747
  const byStatus = {};
@@ -15874,16 +15879,13 @@ function collectSprintSummaryData(store, sprintId) {
15874
15879
  title: d.frontmatter.title,
15875
15880
  type: d.frontmatter.type
15876
15881
  }));
15877
- const riskBlockers = allDocs.filter(
15878
- (d) => !DONE_STATUSES3.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)
15879
- );
15880
- for (const d of riskBlockers) {
15881
- blockers.push({
15882
- id: d.frontmatter.id,
15883
- title: d.frontmatter.title,
15884
- type: d.frontmatter.type
15885
- });
15886
- }
15882
+ const risks = allDocs.filter(
15883
+ (d) => !DONE_STATUSES3.has(d.frontmatter.status) && d.frontmatter.tags?.includes("risk") && d.frontmatter.tags?.some((t) => relevantTags.has(t))
15884
+ ).map((d) => ({
15885
+ id: d.frontmatter.id,
15886
+ title: d.frontmatter.title,
15887
+ type: d.frontmatter.type
15888
+ }));
15887
15889
  let velocity = null;
15888
15890
  const currentRate = workItems.completionPct;
15889
15891
  const completedSprints = sprintDocs.filter((s) => DONE_STATUSES3.has(s.frontmatter.status) && s.frontmatter.id !== fm.id).sort((a, b) => (b.frontmatter.endDate ?? "").localeCompare(a.frontmatter.endDate ?? ""));
@@ -15919,6 +15921,7 @@ function collectSprintSummaryData(store, sprintId) {
15919
15921
  openActions,
15920
15922
  openQuestions,
15921
15923
  blockers,
15924
+ risks,
15922
15925
  velocity
15923
15926
  };
15924
15927
  }
@@ -16714,6 +16717,7 @@ a:hover { text-decoration: underline; }
16714
16717
  }
16715
16718
 
16716
16719
  .card a { color: inherit; text-decoration: none; display: block; }
16720
+ a.card-link { color: inherit; text-decoration: none; cursor: pointer; }
16717
16721
 
16718
16722
  .card .card-label {
16719
16723
  font-size: 0.75rem;
@@ -17129,6 +17133,68 @@ tr:hover td {
17129
17133
  .priority-medium { color: var(--amber); }
17130
17134
  .priority-low { color: var(--green); }
17131
17135
 
17136
+ /* Blocker / Risk detail cards */
17137
+ .blocker-card {
17138
+ background: var(--bg-card);
17139
+ border: 1px solid var(--border);
17140
+ border-radius: var(--radius);
17141
+ padding: 1.25rem;
17142
+ margin-bottom: 1rem;
17143
+ }
17144
+ .blocker-card-header {
17145
+ display: flex;
17146
+ align-items: center;
17147
+ gap: 0.5rem;
17148
+ font-size: 0.85rem;
17149
+ margin-bottom: 0.25rem;
17150
+ }
17151
+ .blocker-card-title {
17152
+ margin: 0.25rem 0 0.5rem;
17153
+ font-size: 1rem;
17154
+ }
17155
+ .blocker-card-meta {
17156
+ display: flex;
17157
+ flex-wrap: wrap;
17158
+ gap: 1rem;
17159
+ font-size: 0.85rem;
17160
+ color: var(--text-dim);
17161
+ margin-bottom: 0.75rem;
17162
+ }
17163
+ .blocker-card-content {
17164
+ border-top: 1px solid var(--border);
17165
+ padding-top: 0.75rem;
17166
+ font-size: 0.9rem;
17167
+ }
17168
+ .risk-assessment-content {
17169
+ background: var(--bg);
17170
+ border: 1px solid var(--border);
17171
+ border-left: 3px solid var(--amber);
17172
+ border-radius: var(--radius);
17173
+ padding: 1rem 1.25rem;
17174
+ margin-top: 0.75rem;
17175
+ font-size: 0.9rem;
17176
+ }
17177
+ .risk-assess-btn {
17178
+ font-size: 0.8rem;
17179
+ padding: 0.4rem 0.8rem;
17180
+ margin-top: 0.75rem;
17181
+ }
17182
+ .risk-assess-loading {
17183
+ margin-top: 0.75rem;
17184
+ font-size: 0.85rem;
17185
+ }
17186
+ .risk-assess-error {
17187
+ margin-top: 0.5rem;
17188
+ }
17189
+ .risk-assessment-label {
17190
+ font-size: 0.7rem;
17191
+ text-transform: uppercase;
17192
+ letter-spacing: 0.08em;
17193
+ color: var(--amber);
17194
+ font-weight: 600;
17195
+ margin-bottom: 0.5rem;
17196
+ }
17197
+
17132
17198
  /* Health */
17133
17199
  .health-section-title {
17134
17200
  font-size: 1.1rem;
@@ -18356,6 +18422,13 @@ function buildPrompt(data) {
18356
18422
  sections.push(`- ${b.id} (${b.type}): ${b.title}`);
18357
18423
  }
18358
18424
  }
18425
+ if (data.risks.length > 0) {
18426
+ sections.push(`
18427
+ ## Risks`);
18428
+ for (const r of data.risks) {
18429
+ sections.push(`- ${r.id} (${r.type}): ${r.title}`);
18430
+ }
18431
+ }
18359
18432
  if (data.velocity) {
18360
18433
  sections.push(`
18361
18434
  ## Velocity`);
@@ -18367,6 +18440,134 @@ function buildPrompt(data) {
18367
18440
  return sections.join("\n");
18368
18441
  }
18369
18442
 
18443
+ // src/reports/sprint-summary/risk-assessment.ts
18444
+ import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
18445
+ var SYSTEM_PROMPT2 = `You are a delivery management assistant generating a data-driven risk assessment.
18446
+
18447
+ 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.
18448
+
18449
+ Produce a concise markdown assessment with these sections:
18450
+
18451
+ ## Status Assessment
18452
+ One-line verdict: is this risk actively being mitigated, stalled, or escalating?
18453
+
18454
+ ## Related Activity
18455
+ What actions, decisions, or contributions are connected to this risk? How are they progressing? Be specific \u2014 reference artifact IDs from the data provided.
18456
+
18457
+ ## Trajectory
18458
+ 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.
18459
+
18460
+ ## Recommendation
18461
+ One concrete next step to move this risk toward resolution.
18462
+
18463
+ Rules:
18464
+ - Reference artifact IDs, dates, owners, and statuses from the provided data
18465
+ - Keep the tone professional and direct
18466
+ - Do NOT speculate beyond what the data supports \u2014 if information is insufficient, say so explicitly
18467
+ - Do NOT ask for more information or say you will look things up \u2014 everything you need is in the prompt
18468
+ - Produce the full assessment text directly`;
18469
+ async function generateRiskAssessment(data, riskId, store) {
18470
+ const risk = data.risks.find((r) => r.id === riskId);
18471
+ if (!risk) return "Risk not found in sprint data.";
18472
+ const prompt = buildSingleRiskPrompt(data, risk, store);
18473
+ const result = query2({
18474
+ prompt,
18475
+ options: {
18476
+ systemPrompt: SYSTEM_PROMPT2,
18477
+ maxTurns: 1,
18478
+ tools: [],
18479
+ allowedTools: []
18480
+ }
18481
+ });
18482
+ for await (const msg of result) {
18483
+ if (msg.type === "assistant") {
18484
+ const text = msg.message.content.find(
18485
+ (b) => b.type === "text"
18486
+ );
18487
+ if (text) return text.text;
18488
+ }
18489
+ }
18490
+ return "Unable to generate risk assessment.";
18491
+ }
18492
+ function buildSingleRiskPrompt(data, risk, store) {
18493
+ const sections = [];
18494
+ sections.push(`# Sprint: ${data.sprint.id} \u2014 ${data.sprint.title}`);
18495
+ if (data.sprint.goal) sections.push(`Goal: ${data.sprint.goal}`);
18496
+ sections.push(`Days remaining: ${data.timeline.daysRemaining} / ${data.timeline.totalDays}`);
18497
+ sections.push(`Completion: ${data.workItems.completionPct}%`);
18498
+ sections.push("");
18499
+ const doc = store.get(risk.id);
18500
+ sections.push(`# RISK: ${risk.id} \u2014 ${risk.title}`);
18501
+ sections.push(`Type: ${risk.type}`);
18502
+ if (doc) {
18503
+ sections.push(`Status: ${doc.frontmatter.status}`);
18504
+ if (doc.frontmatter.owner) sections.push(`Owner: ${doc.frontmatter.owner}`);
18505
+ if (doc.frontmatter.assignee) sections.push(`Assignee: ${doc.frontmatter.assignee}`);
18506
+ if (doc.frontmatter.priority) sections.push(`Priority: ${doc.frontmatter.priority}`);
18507
+ if (doc.frontmatter.dueDate) sections.push(`Due date: ${doc.frontmatter.dueDate}`);
18508
+ if (doc.frontmatter.created) sections.push(`Created: ${doc.frontmatter.created.slice(0, 10)}`);
18509
+ const tags = doc.frontmatter.tags ?? [];
18510
+ if (tags.length > 0) sections.push(`Tags: ${tags.join(", ")}`);
18511
+ if (doc.content.trim()) {
18512
+ sections.push(`
18513
+ Description:
18514
+ ${doc.content.trim()}`);
18515
+ }
18516
+ const allDocs = store.list();
18517
+ const relatedIds = /* @__PURE__ */ new Set();
18518
+ for (const d of allDocs) {
18519
+ if (d.frontmatter.aboutArtifact === risk.id) {
18520
+ relatedIds.add(d.frontmatter.id);
18521
+ }
18522
+ }
18523
+ const idPattern = /\b([A-Z]-\d{3,})\b/g;
18524
+ let match;
18525
+ while ((match = idPattern.exec(doc.content)) !== null) {
18526
+ relatedIds.add(match[1]);
18527
+ }
18528
+ const significantTags = tags.filter(
18529
+ (t) => !t.startsWith("sprint:") && !t.startsWith("focus:") && t !== "risk"
18530
+ );
18531
+ if (significantTags.length > 0) {
18532
+ for (const d of allDocs) {
18533
+ if (d.frontmatter.id === risk.id) continue;
18534
+ const dTags = d.frontmatter.tags ?? [];
18535
+ if (significantTags.some((t) => dTags.includes(t))) {
18536
+ relatedIds.add(d.frontmatter.id);
18537
+ }
18538
+ }
18539
+ }
18540
+ const about = doc.frontmatter.aboutArtifact;
18541
+ if (about) {
18542
+ relatedIds.add(about);
18543
+ for (const d of allDocs) {
18544
+ if (d.frontmatter.aboutArtifact === about && d.frontmatter.id !== risk.id) {
18545
+ relatedIds.add(d.frontmatter.id);
18546
+ }
18547
+ }
18548
+ }
18549
+ const relatedDocs = [...relatedIds].map((id) => store.get(id)).filter((d) => d != null).slice(0, 20);
18550
+ if (relatedDocs.length > 0) {
18551
+ sections.push(`
18552
+ ## Related Documents (${relatedDocs.length})`);
18553
+ for (const rd of relatedDocs) {
18554
+ const owner = rd.frontmatter.owner ?? "unowned";
18555
+ const summary = rd.content.trim().slice(0, 300);
18556
+ sections.push(
18557
+ `- ${rd.frontmatter.id} (${rd.frontmatter.type}) [${rd.frontmatter.status}] \u2014 ${rd.frontmatter.title}`
18558
+ );
18559
+ sections.push(` Owner: ${owner}${rd.frontmatter.dueDate ? `, Due: ${rd.frontmatter.dueDate}` : ""}`);
18560
+ if (summary) sections.push(` Summary: ${summary}${rd.content.trim().length > 300 ? "..." : ""}`);
18561
+ }
18562
+ }
18563
+ }
18564
+ sections.push("");
18565
+ sections.push(`---`);
18566
+ sections.push(`
18567
+ Generate the risk assessment for ${risk.id} based on the data above.`);
18568
+ return sections.join("\n");
18569
+ }
18570
+
18370
18571
  // src/web/templates/persona-switcher.ts
18371
18572
  function renderPersonaSwitcher(current, _currentPath) {
18372
18573
  const views = getAllPersonaViews();
@@ -19626,11 +19827,16 @@ function sprintSummaryPage(data, cached2) {
19626
19827
  <div class="card-value">${data.linkedEpics.length}</div>
19627
19828
  <div class="card-sub">linked to sprint</div>
19628
19829
  </div>
19629
- <div class="card">
19830
+ <a class="card card-link" href="sprint-blockers">
19630
19831
  <div class="card-label">Blockers</div>
19631
19832
  <div class="card-value${data.blockers.length > 0 ? " priority-high" : ""}">${data.blockers.length}</div>
19632
- <div class="card-sub">${data.openActions.length} open actions</div>
19633
- </div>
19833
+ <div class="card-sub">${data.workItems.blocked} blocked items</div>
19834
+ </a>
19835
+ <a class="card card-link" href="sprint-risks">
19836
+ <div class="card-label">Risks</div>
19837
+ <div class="card-value${data.risks.length > 0 ? " priority-medium" : ""}">${data.risks.length}</div>
19838
+ <div class="card-sub">open risk items</div>
19839
+ </a>
19634
19840
  </div>`;
19635
19841
  const epicsTable = data.linkedEpics.length > 0 ? collapsibleSection(
19636
19842
  "ss-epics",
@@ -21020,6 +21226,168 @@ function upcomingPage(data) {
21020
21226
  `;
21021
21227
  }
21022
21228
 
21229
+ // src/web/templates/pages/sprint-blockers.ts
21230
+ function sprintBlockersPage(data, store) {
21231
+ if (!data) {
21232
+ return `
21233
+ <div class="page-header">
21234
+ <h2>Sprint Blockers</h2>
21235
+ <div class="subtitle">Blocked items in the active sprint</div>
21236
+ </div>
21237
+ <div class="empty">
21238
+ <h3>No Active Sprint</h3>
21239
+ <p>No active sprint found.</p>
21240
+ </div>`;
21241
+ }
21242
+ const blockerDocs = data.blockers.map((b) => {
21243
+ const doc = store.get(b.id);
21244
+ return { ...b, doc };
21245
+ });
21246
+ const statsCards = `
21247
+ <div class="cards">
21248
+ <div class="card">
21249
+ <div class="card-label">Blocked Items</div>
21250
+ <div class="card-value${blockerDocs.length > 0 ? " priority-high" : ""}">${blockerDocs.length}</div>
21251
+ <div class="card-sub">in ${escapeHtml(data.sprint.id)}</div>
21252
+ </div>
21253
+ </div>`;
21254
+ const itemCards = blockerDocs.map((b) => {
21255
+ const doc = b.doc;
21256
+ const owner = doc?.frontmatter.owner;
21257
+ const assignee = doc?.frontmatter.assignee;
21258
+ const content = doc?.content?.trim();
21259
+ return `
21260
+ <div class="blocker-card">
21261
+ <div class="blocker-card-header">
21262
+ <a href="/docs/${escapeHtml(b.type)}/${escapeHtml(b.id)}">${escapeHtml(b.id)}</a>
21263
+ <span class="text-dim">${escapeHtml(typeLabel(b.type))}</span>
21264
+ ${statusBadge("blocked")}
21265
+ </div>
21266
+ <h4 class="blocker-card-title">${escapeHtml(b.title)}</h4>
21267
+ <div class="blocker-card-meta">
21268
+ ${owner ? `<span><strong>Owner:</strong> ${escapeHtml(owner)}</span>` : ""}
21269
+ ${assignee ? `<span><strong>Assignee:</strong> ${escapeHtml(assignee)}</span>` : ""}
21270
+ ${doc?.frontmatter.created ? `<span><strong>Created:</strong> ${formatDate(doc.frontmatter.created)}</span>` : ""}
21271
+ </div>
21272
+ ${content ? `<div class="blocker-card-content detail-content">${renderMarkdown(content)}</div>` : ""}
21273
+ </div>`;
21274
+ }).join("");
21275
+ const emptyMessage = blockerDocs.length === 0 ? `<div class="empty"><h3>No Blockers</h3><p>No blocked items in this sprint.</p></div>` : "";
21276
+ return `
21277
+ <div class="page-header">
21278
+ <h2>Sprint Blockers</h2>
21279
+ <div class="subtitle">Blocked items in ${escapeHtml(data.sprint.id)} \u2014 ${escapeHtml(data.sprint.title)}</div>
21280
+ </div>
21281
+ ${statsCards}
21282
+ ${emptyMessage}
21283
+ ${itemCards}`;
21284
+ }
21285
+
21286
+ // src/web/templates/pages/sprint-risks.ts
21287
+ function sprintRisksPage(data, store) {
21288
+ if (!data) {
21289
+ return `
21290
+ <div class="page-header">
21291
+ <h2>Sprint Risks</h2>
21292
+ <div class="subtitle">Risk items in the active sprint</div>
21293
+ </div>
21294
+ <div class="empty">
21295
+ <h3>No Active Sprint</h3>
21296
+ <p>No active sprint found.</p>
21297
+ </div>`;
21298
+ }
21299
+ const riskDocs = data.risks.map((r) => {
21300
+ const doc = store.get(r.id);
21301
+ return { ...r, doc };
21302
+ });
21303
+ const statsCards = `
21304
+ <div class="cards">
21305
+ <div class="card">
21306
+ <div class="card-label">Open Risks</div>
21307
+ <div class="card-value${riskDocs.length > 0 ? " priority-medium" : ""}">${riskDocs.length}</div>
21308
+ <div class="card-sub">in ${escapeHtml(data.sprint.id)}</div>
21309
+ </div>
21310
+ </div>`;
21311
+ const itemCards = riskDocs.map((r) => {
21312
+ const doc = r.doc;
21313
+ const owner = doc?.frontmatter.owner;
21314
+ const assignee = doc?.frontmatter.assignee;
21315
+ const content = doc?.content?.trim();
21316
+ return `
21317
+ <div class="blocker-card" id="risk-${escapeHtml(r.id)}">
21318
+ <div class="blocker-card-header">
21319
+ <a href="/docs/${escapeHtml(r.type)}/${escapeHtml(r.id)}">${escapeHtml(r.id)}</a>
21320
+ <span class="text-dim">${escapeHtml(typeLabel(r.type))}</span>
21321
+ ${statusBadge(doc?.frontmatter.status ?? "open")}
21322
+ </div>
21323
+ <h4 class="blocker-card-title">${escapeHtml(r.title)}</h4>
21324
+ <div class="blocker-card-meta">
21325
+ ${owner ? `<span><strong>Owner:</strong> ${escapeHtml(owner)}</span>` : ""}
21326
+ ${assignee ? `<span><strong>Assignee:</strong> ${escapeHtml(assignee)}</span>` : ""}
21327
+ ${doc?.frontmatter.created ? `<span><strong>Created:</strong> ${formatDate(doc.frontmatter.created)}</span>` : ""}
21328
+ </div>
21329
+ ${content ? `<div class="blocker-card-content detail-content">${renderMarkdown(content)}</div>` : ""}
21330
+ <div class="risk-assessment" id="assessment-${escapeHtml(r.id)}">
21331
+ <button class="sprint-generate-btn risk-assess-btn" onclick="generateAssessment('${escapeHtml(r.id)}', this)">Assess Risk</button>
21332
+ <div class="sprint-loading risk-assess-loading" style="display:none">
21333
+ <div class="sprint-spinner"></div>
21334
+ <span>Analyzing...</span>
21335
+ </div>
21336
+ <div class="sprint-error risk-assess-error" style="display:none"></div>
21337
+ <div class="risk-assessment-content detail-content" style="display:none"></div>
21338
+ </div>
21339
+ </div>`;
21340
+ }).join("");
21341
+ const emptyMessage = riskDocs.length === 0 ? `<div class="empty"><h3>No Risks</h3><p>No open risk items in this sprint.</p></div>` : "";
21342
+ const script = riskDocs.length > 0 ? `
21343
+ <script>
21344
+ async function generateAssessment(riskId, btn) {
21345
+ var container = document.getElementById('assessment-' + riskId);
21346
+ var loading = container.querySelector('.risk-assess-loading');
21347
+ var errorEl = container.querySelector('.risk-assess-error');
21348
+ var contentEl = container.querySelector('.risk-assessment-content');
21349
+
21350
+ btn.disabled = true;
21351
+ btn.style.display = 'none';
21352
+ loading.style.display = 'flex';
21353
+ errorEl.style.display = 'none';
21354
+
21355
+ try {
21356
+ var res = await fetch('/api/risk-assessment', {
21357
+ method: 'POST',
21358
+ headers: { 'Content-Type': 'application/json' },
21359
+ body: JSON.stringify({ sprintId: '${escapeHtml(data.sprint.id)}', riskId: riskId })
21360
+ });
21361
+ var json = await res.json();
21362
+ if (!res.ok) throw new Error(json.error || 'Failed to generate assessment');
21363
+
21364
+ contentEl.innerHTML = '<div class="risk-assessment-label">AI Assessment</div>' + json.html;
21365
+ contentEl.style.display = 'block';
21366
+
21367
+ loading.style.display = 'none';
21368
+ btn.textContent = 'Regenerate';
21369
+ btn.style.display = '';
21370
+ btn.disabled = false;
21371
+ } catch (e) {
21372
+ loading.style.display = 'none';
21373
+ errorEl.textContent = e.message;
21374
+ errorEl.style.display = 'block';
21375
+ btn.style.display = '';
21376
+ btn.disabled = false;
21377
+ }
21378
+ }
21379
+ </script>` : "";
21380
+ return `
21381
+ <div class="page-header">
21382
+ <h2>Sprint Risks</h2>
21383
+ <div class="subtitle">Risk items in ${escapeHtml(data.sprint.id)} \u2014 ${escapeHtml(data.sprint.title)}</div>
21384
+ </div>
21385
+ ${statsCards}
21386
+ ${emptyMessage}
21387
+ ${itemCards}
21388
+ ${script}`;
21389
+ }
21390
+
21023
21391
  // src/web/templates/pages/shared-wrappers.ts
21024
21392
  function sharedTimelinePage(ctx) {
21025
21393
  const diagrams = getDiagramData(ctx.store);
@@ -21049,6 +21417,16 @@ function sharedSprintSummaryPage(ctx) {
21049
21417
  const data = getSprintSummaryData(ctx.store, sprintId);
21050
21418
  return sprintSummaryPage(data);
21051
21419
  }
21420
+ function sharedSprintBlockersPage(ctx) {
21421
+ const sprintId = ctx.searchParams?.get("sprint") ?? void 0;
21422
+ const data = getSprintSummaryData(ctx.store, sprintId);
21423
+ return sprintBlockersPage(data, ctx.store);
21424
+ }
21425
+ function sharedSprintRisksPage(ctx) {
21426
+ const sprintId = ctx.searchParams?.get("sprint") ?? void 0;
21427
+ const data = getSprintSummaryData(ctx.store, sprintId);
21428
+ return sprintRisksPage(data, ctx.store);
21429
+ }
21052
21430
 
21053
21431
  // src/web/shared-page-registration.ts
21054
21432
  var SHARED_PAGES = [
@@ -21057,7 +21435,9 @@ var SHARED_PAGES = [
21057
21435
  { pageId: "upcoming", renderer: sharedUpcomingPage },
21058
21436
  { pageId: "gar", renderer: sharedGarPage },
21059
21437
  { pageId: "health", renderer: sharedHealthPage },
21060
- { pageId: "sprint-summary", renderer: sharedSprintSummaryPage }
21438
+ { pageId: "sprint-summary", renderer: sharedSprintSummaryPage },
21439
+ { pageId: "sprint-blockers", renderer: sharedSprintBlockersPage },
21440
+ { pageId: "sprint-risks", renderer: sharedSprintRisksPage }
21061
21441
  ];
21062
21442
  for (const persona of ["po", "dm", "tl"]) {
21063
21443
  for (const { pageId, renderer } of SHARED_PAGES) {
@@ -21258,6 +21638,37 @@ function handleRequest(req, res, store, projectName, navGroups) {
21258
21638
  });
21259
21639
  return;
21260
21640
  }
21641
+ if (pathname === "/api/risk-assessment" && req.method === "POST") {
21642
+ let bodyStr = "";
21643
+ req.on("data", (chunk) => {
21644
+ bodyStr += chunk;
21645
+ });
21646
+ req.on("end", async () => {
21647
+ try {
21648
+ const { sprintId, riskId } = JSON.parse(bodyStr || "{}");
21649
+ if (!riskId) {
21650
+ res.writeHead(400, { "Content-Type": "application/json" });
21651
+ res.end(JSON.stringify({ error: "riskId is required" }));
21652
+ return;
21653
+ }
21654
+ const data = getSprintSummaryData(store, sprintId);
21655
+ if (!data) {
21656
+ res.writeHead(404, { "Content-Type": "application/json" });
21657
+ res.end(JSON.stringify({ error: "Sprint not found" }));
21658
+ return;
21659
+ }
21660
+ const markdown = await generateRiskAssessment(data, riskId, store);
21661
+ const html = renderMarkdown(markdown);
21662
+ res.writeHead(200, { "Content-Type": "application/json" });
21663
+ res.end(JSON.stringify({ riskId, html }));
21664
+ } catch (err) {
21665
+ console.error("[marvin web] Risk assessment generation error:", err);
21666
+ res.writeHead(500, { "Content-Type": "application/json" });
21667
+ res.end(JSON.stringify({ error: "Failed to generate risk assessment" }));
21668
+ }
21669
+ });
21670
+ return;
21671
+ }
21261
21672
  const detailMatch = pathname.match(/^\/docs\/([^/]+)\/([^/]+)$/);
21262
21673
  if (detailMatch) {
21263
21674
  const [, type, id] = detailMatch;
@@ -26041,7 +26452,7 @@ import * as readline from "readline";
26041
26452
  import chalk from "chalk";
26042
26453
  import ora from "ora";
26043
26454
  import {
26044
- query as query3
26455
+ query as query4
26045
26456
  } from "@anthropic-ai/claude-agent-sdk";
26046
26457
 
26047
26458
  // src/storage/session-store.ts
@@ -26112,11 +26523,11 @@ var SessionStore = class {
26112
26523
  };
26113
26524
 
26114
26525
  // src/agent/session-namer.ts
26115
- import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
26526
+ import { query as query3 } from "@anthropic-ai/claude-agent-sdk";
26116
26527
  async function generateSessionName(turns) {
26117
26528
  try {
26118
26529
  const transcript = turns.slice(-20).map((t) => `${t.role}: ${t.content.slice(0, 200)}`).join("\n");
26119
- const result = query2({
26530
+ const result = query3({
26120
26531
  prompt: `Summarize this conversation in 3-5 words as a kebab-case name suitable for a filename. Output ONLY the name, nothing else.
26121
26532
 
26122
26533
  ${transcript}`,
@@ -26394,7 +26805,7 @@ Marvin \u2014 ${persona.name}
26394
26805
  if (existingSession) {
26395
26806
  queryOptions.resume = existingSession.id;
26396
26807
  }
26397
- const conversation = query3({
26808
+ const conversation = query4({
26398
26809
  prompt,
26399
26810
  options: queryOptions
26400
26811
  });
@@ -26486,7 +26897,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
26486
26897
  import { tool as tool24 } from "@anthropic-ai/claude-agent-sdk";
26487
26898
 
26488
26899
  // src/skills/action-runner.ts
26489
- import { query as query4 } from "@anthropic-ai/claude-agent-sdk";
26900
+ import { query as query5 } from "@anthropic-ai/claude-agent-sdk";
26490
26901
  var GOVERNANCE_TOOL_NAMES2 = [
26491
26902
  "mcp__marvin-governance__list_decisions",
26492
26903
  "mcp__marvin-governance__get_decision",
@@ -26508,7 +26919,7 @@ async function runSkillAction(action, userPrompt, context) {
26508
26919
  try {
26509
26920
  const mcpServer = createMarvinMcpServer(context.store);
26510
26921
  const allowedTools = action.allowGovernanceTools !== false ? GOVERNANCE_TOOL_NAMES2 : [];
26511
- const conversation = query4({
26922
+ const conversation = query5({
26512
26923
  prompt: userPrompt,
26513
26924
  options: {
26514
26925
  systemPrompt: action.systemPrompt,
@@ -27302,7 +27713,7 @@ import * as fs13 from "fs";
27302
27713
  import * as path13 from "path";
27303
27714
  import chalk7 from "chalk";
27304
27715
  import ora2 from "ora";
27305
- import { query as query5 } from "@anthropic-ai/claude-agent-sdk";
27716
+ import { query as query6 } from "@anthropic-ai/claude-agent-sdk";
27306
27717
 
27307
27718
  // src/sources/prompts.ts
27308
27719
  function buildIngestSystemPrompt(persona, projectConfig, isDraft) {
@@ -27435,7 +27846,7 @@ async function ingestFile(options) {
27435
27846
  const spinner = ora2({ text: `Analyzing ${fileName}...`, color: "cyan" });
27436
27847
  spinner.start();
27437
27848
  try {
27438
- const conversation = query5({
27849
+ const conversation = query6({
27439
27850
  prompt: userPrompt,
27440
27851
  options: {
27441
27852
  systemPrompt,
@@ -28656,7 +29067,7 @@ import chalk13 from "chalk";
28656
29067
  // src/analysis/analyze.ts
28657
29068
  import chalk12 from "chalk";
28658
29069
  import ora4 from "ora";
28659
- import { query as query6 } from "@anthropic-ai/claude-agent-sdk";
29070
+ import { query as query7 } from "@anthropic-ai/claude-agent-sdk";
28660
29071
 
28661
29072
  // src/analysis/prompts.ts
28662
29073
  function buildAnalyzeSystemPrompt(persona, projectConfig, isDraft) {
@@ -28786,7 +29197,7 @@ async function analyzeMeeting(options) {
28786
29197
  const spinner = ora4({ text: `Analyzing meeting ${meetingId}...`, color: "cyan" });
28787
29198
  spinner.start();
28788
29199
  try {
28789
- const conversation = query6({
29200
+ const conversation = query7({
28790
29201
  prompt: userPrompt,
28791
29202
  options: {
28792
29203
  systemPrompt,
@@ -28913,7 +29324,7 @@ import chalk15 from "chalk";
28913
29324
  // src/contributions/contribute.ts
28914
29325
  import chalk14 from "chalk";
28915
29326
  import ora5 from "ora";
28916
- import { query as query7 } from "@anthropic-ai/claude-agent-sdk";
29327
+ import { query as query8 } from "@anthropic-ai/claude-agent-sdk";
28917
29328
 
28918
29329
  // src/contributions/prompts.ts
28919
29330
  function buildContributeSystemPrompt(persona, contributionType, projectConfig, isDraft) {
@@ -29167,7 +29578,7 @@ async function contributeFromPersona(options) {
29167
29578
  "mcp__marvin-governance__get_action",
29168
29579
  "mcp__marvin-governance__get_question"
29169
29580
  ];
29170
- const conversation = query7({
29581
+ const conversation = query8({
29171
29582
  prompt: userPrompt,
29172
29583
  options: {
29173
29584
  systemPrompt,
@@ -29663,7 +30074,7 @@ function createProgram() {
29663
30074
  const program = new Command();
29664
30075
  program.name("marvin").description(
29665
30076
  "AI-powered product development assistant with Product Owner, Delivery Manager, and Technical Lead personas"
29666
- ).version("0.5.3");
30077
+ ).version("0.5.5");
29667
30078
  program.command("init").description("Initialize a new Marvin project in the current directory").action(async () => {
29668
30079
  await initCommand();
29669
30080
  });