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/marvin.js CHANGED
@@ -14636,7 +14636,7 @@ function collectSprintSummaryData(store, sprintId) {
14636
14636
  });
14637
14637
  const sprintTag = `sprint:${fm.id}`;
14638
14638
  const workItemDocs = allDocs.filter(
14639
- (d) => d.frontmatter.type !== "sprint" && d.frontmatter.type !== "epic" && d.frontmatter.type !== "meeting" && d.frontmatter.tags?.includes(sprintTag)
14639
+ (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)
14640
14640
  );
14641
14641
  const primaryDocs = workItemDocs.filter((d) => d.frontmatter.type !== "contribution");
14642
14642
  const byStatus = {};
@@ -14774,16 +14774,13 @@ function collectSprintSummaryData(store, sprintId) {
14774
14774
  title: d.frontmatter.title,
14775
14775
  type: d.frontmatter.type
14776
14776
  }));
14777
- const riskBlockers = allDocs.filter(
14778
- (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)
14779
- );
14780
- for (const d of riskBlockers) {
14781
- blockers.push({
14782
- id: d.frontmatter.id,
14783
- title: d.frontmatter.title,
14784
- type: d.frontmatter.type
14785
- });
14786
- }
14777
+ const risks = allDocs.filter(
14778
+ (d) => !DONE_STATUSES2.has(d.frontmatter.status) && d.frontmatter.tags?.includes("risk") && d.frontmatter.tags?.some((t) => relevantTags.has(t))
14779
+ ).map((d) => ({
14780
+ id: d.frontmatter.id,
14781
+ title: d.frontmatter.title,
14782
+ type: d.frontmatter.type
14783
+ }));
14787
14784
  let velocity = null;
14788
14785
  const currentRate = workItems.completionPct;
14789
14786
  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 ?? ""));
@@ -14819,6 +14816,7 @@ function collectSprintSummaryData(store, sprintId) {
14819
14816
  openActions,
14820
14817
  openQuestions,
14821
14818
  blockers,
14819
+ risks,
14822
14820
  velocity
14823
14821
  };
14824
14822
  }
@@ -15404,6 +15402,13 @@ function buildPrompt(data) {
15404
15402
  sections.push(`- ${b.id} (${b.type}): ${b.title}`);
15405
15403
  }
15406
15404
  }
15405
+ if (data.risks.length > 0) {
15406
+ sections.push(`
15407
+ ## Risks`);
15408
+ for (const r of data.risks) {
15409
+ sections.push(`- ${r.id} (${r.type}): ${r.title}`);
15410
+ }
15411
+ }
15407
15412
  if (data.velocity) {
15408
15413
  sections.push(`
15409
15414
  ## Velocity`);
@@ -18252,7 +18257,7 @@ import * as readline from "readline";
18252
18257
  import chalk2 from "chalk";
18253
18258
  import ora from "ora";
18254
18259
  import {
18255
- query as query3
18260
+ query as query4
18256
18261
  } from "@anthropic-ai/claude-agent-sdk";
18257
18262
 
18258
18263
  // src/personas/prompt-builder.ts
@@ -18392,9 +18397,9 @@ var DocumentStore = class {
18392
18397
  }
18393
18398
  }
18394
18399
  }
18395
- list(query8) {
18400
+ list(query9) {
18396
18401
  const results = [];
18397
- const types = query8?.type ? [query8.type] : Object.keys(this.typeDirs);
18402
+ const types = query9?.type ? [query9.type] : Object.keys(this.typeDirs);
18398
18403
  for (const type of types) {
18399
18404
  const dirName = this.typeDirs[type];
18400
18405
  if (!dirName) continue;
@@ -18405,9 +18410,9 @@ var DocumentStore = class {
18405
18410
  const filePath = path6.join(dir, file2);
18406
18411
  const raw = fs6.readFileSync(filePath, "utf-8");
18407
18412
  const doc = parseDocument(raw, filePath);
18408
- if (query8?.status && doc.frontmatter.status !== query8.status) continue;
18409
- if (query8?.owner && doc.frontmatter.owner !== query8.owner) continue;
18410
- if (query8?.tag && (!doc.frontmatter.tags || !doc.frontmatter.tags.includes(query8.tag)))
18413
+ if (query9?.status && doc.frontmatter.status !== query9.status) continue;
18414
+ if (query9?.owner && doc.frontmatter.owner !== query9.owner) continue;
18415
+ if (query9?.tag && (!doc.frontmatter.tags || !doc.frontmatter.tags.includes(query9.tag)))
18411
18416
  continue;
18412
18417
  results.push(doc);
18413
18418
  }
@@ -18485,14 +18490,19 @@ var DocumentStore = class {
18485
18490
  if (!existing) {
18486
18491
  throw new Error(`Document ${id} not found`);
18487
18492
  }
18493
+ const keysToDelete = Object.entries(updates).filter(([, v]) => v === void 0).map(([k]) => k);
18488
18494
  const cleanedUpdates = Object.fromEntries(
18489
18495
  Object.entries(updates).filter(([, v]) => v !== void 0)
18490
18496
  );
18491
- const updatedFrontmatter = {
18497
+ const merged = {
18492
18498
  ...existing.frontmatter,
18493
18499
  ...cleanedUpdates,
18494
18500
  updated: (/* @__PURE__ */ new Date()).toISOString()
18495
18501
  };
18502
+ for (const key of keysToDelete) {
18503
+ delete merged[key];
18504
+ }
18505
+ const updatedFrontmatter = merged;
18496
18506
  const doc = {
18497
18507
  frontmatter: updatedFrontmatter,
18498
18508
  content: content ?? existing.content,
@@ -19777,6 +19787,7 @@ a:hover { text-decoration: underline; }
19777
19787
  }
19778
19788
 
19779
19789
  .card a { color: inherit; text-decoration: none; display: block; }
19790
+ a.card-link { color: inherit; text-decoration: none; cursor: pointer; }
19780
19791
 
19781
19792
  .card .card-label {
19782
19793
  font-size: 0.75rem;
@@ -20192,6 +20203,68 @@ tr:hover td {
20192
20203
  .priority-medium { color: var(--amber); }
20193
20204
  .priority-low { color: var(--green); }
20194
20205
 
20206
+ /* Blocker / Risk detail cards */
20207
+ .blocker-card {
20208
+ background: var(--bg-card);
20209
+ border: 1px solid var(--border);
20210
+ border-radius: var(--radius);
20211
+ padding: 1.25rem;
20212
+ margin-bottom: 1rem;
20213
+ }
20214
+ .blocker-card-header {
20215
+ display: flex;
20216
+ align-items: center;
20217
+ gap: 0.5rem;
20218
+ font-size: 0.85rem;
20219
+ margin-bottom: 0.25rem;
20220
+ }
20221
+ .blocker-card-title {
20222
+ margin: 0.25rem 0 0.5rem;
20223
+ font-size: 1rem;
20224
+ }
20225
+ .blocker-card-meta {
20226
+ display: flex;
20227
+ flex-wrap: wrap;
20228
+ gap: 1rem;
20229
+ font-size: 0.85rem;
20230
+ color: var(--text-dim);
20231
+ margin-bottom: 0.75rem;
20232
+ }
20233
+ .blocker-card-content {
20234
+ border-top: 1px solid var(--border);
20235
+ padding-top: 0.75rem;
20236
+ font-size: 0.9rem;
20237
+ }
20238
+ .risk-assessment-content {
20239
+ background: var(--bg);
20240
+ border: 1px solid var(--border);
20241
+ border-left: 3px solid var(--amber);
20242
+ border-radius: var(--radius);
20243
+ padding: 1rem 1.25rem;
20244
+ margin-top: 0.75rem;
20245
+ font-size: 0.9rem;
20246
+ }
20247
+ .risk-assess-btn {
20248
+ font-size: 0.8rem;
20249
+ padding: 0.4rem 0.8rem;
20250
+ margin-top: 0.75rem;
20251
+ }
20252
+ .risk-assess-loading {
20253
+ margin-top: 0.75rem;
20254
+ font-size: 0.85rem;
20255
+ }
20256
+ .risk-assess-error {
20257
+ margin-top: 0.5rem;
20258
+ }
20259
+ .risk-assessment-label {
20260
+ font-size: 0.7rem;
20261
+ text-transform: uppercase;
20262
+ letter-spacing: 0.08em;
20263
+ color: var(--amber);
20264
+ font-weight: 600;
20265
+ margin-bottom: 0.5rem;
20266
+ }
20267
+
20195
20268
  /* Health */
20196
20269
  .health-section-title {
20197
20270
  font-size: 1.1rem;
@@ -21308,6 +21381,134 @@ function personaPickerPage() {
21308
21381
  </div>`;
21309
21382
  }
21310
21383
 
21384
+ // src/reports/sprint-summary/risk-assessment.ts
21385
+ import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
21386
+ var SYSTEM_PROMPT2 = `You are a delivery management assistant generating a data-driven risk assessment.
21387
+
21388
+ 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.
21389
+
21390
+ Produce a concise markdown assessment with these sections:
21391
+
21392
+ ## Status Assessment
21393
+ One-line verdict: is this risk actively being mitigated, stalled, or escalating?
21394
+
21395
+ ## Related Activity
21396
+ What actions, decisions, or contributions are connected to this risk? How are they progressing? Be specific \u2014 reference artifact IDs from the data provided.
21397
+
21398
+ ## Trajectory
21399
+ 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.
21400
+
21401
+ ## Recommendation
21402
+ One concrete next step to move this risk toward resolution.
21403
+
21404
+ Rules:
21405
+ - Reference artifact IDs, dates, owners, and statuses from the provided data
21406
+ - Keep the tone professional and direct
21407
+ - Do NOT speculate beyond what the data supports \u2014 if information is insufficient, say so explicitly
21408
+ - Do NOT ask for more information or say you will look things up \u2014 everything you need is in the prompt
21409
+ - Produce the full assessment text directly`;
21410
+ async function generateRiskAssessment(data, riskId, store) {
21411
+ const risk = data.risks.find((r) => r.id === riskId);
21412
+ if (!risk) return "Risk not found in sprint data.";
21413
+ const prompt = buildSingleRiskPrompt(data, risk, store);
21414
+ const result = query2({
21415
+ prompt,
21416
+ options: {
21417
+ systemPrompt: SYSTEM_PROMPT2,
21418
+ maxTurns: 1,
21419
+ tools: [],
21420
+ allowedTools: []
21421
+ }
21422
+ });
21423
+ for await (const msg of result) {
21424
+ if (msg.type === "assistant") {
21425
+ const text = msg.message.content.find(
21426
+ (b) => b.type === "text"
21427
+ );
21428
+ if (text) return text.text;
21429
+ }
21430
+ }
21431
+ return "Unable to generate risk assessment.";
21432
+ }
21433
+ function buildSingleRiskPrompt(data, risk, store) {
21434
+ const sections = [];
21435
+ sections.push(`# Sprint: ${data.sprint.id} \u2014 ${data.sprint.title}`);
21436
+ if (data.sprint.goal) sections.push(`Goal: ${data.sprint.goal}`);
21437
+ sections.push(`Days remaining: ${data.timeline.daysRemaining} / ${data.timeline.totalDays}`);
21438
+ sections.push(`Completion: ${data.workItems.completionPct}%`);
21439
+ sections.push("");
21440
+ const doc = store.get(risk.id);
21441
+ sections.push(`# RISK: ${risk.id} \u2014 ${risk.title}`);
21442
+ sections.push(`Type: ${risk.type}`);
21443
+ if (doc) {
21444
+ sections.push(`Status: ${doc.frontmatter.status}`);
21445
+ if (doc.frontmatter.owner) sections.push(`Owner: ${doc.frontmatter.owner}`);
21446
+ if (doc.frontmatter.assignee) sections.push(`Assignee: ${doc.frontmatter.assignee}`);
21447
+ if (doc.frontmatter.priority) sections.push(`Priority: ${doc.frontmatter.priority}`);
21448
+ if (doc.frontmatter.dueDate) sections.push(`Due date: ${doc.frontmatter.dueDate}`);
21449
+ if (doc.frontmatter.created) sections.push(`Created: ${doc.frontmatter.created.slice(0, 10)}`);
21450
+ const tags = doc.frontmatter.tags ?? [];
21451
+ if (tags.length > 0) sections.push(`Tags: ${tags.join(", ")}`);
21452
+ if (doc.content.trim()) {
21453
+ sections.push(`
21454
+ Description:
21455
+ ${doc.content.trim()}`);
21456
+ }
21457
+ const allDocs = store.list();
21458
+ const relatedIds = /* @__PURE__ */ new Set();
21459
+ for (const d of allDocs) {
21460
+ if (d.frontmatter.aboutArtifact === risk.id) {
21461
+ relatedIds.add(d.frontmatter.id);
21462
+ }
21463
+ }
21464
+ const idPattern = /\b([A-Z]-\d{3,})\b/g;
21465
+ let match;
21466
+ while ((match = idPattern.exec(doc.content)) !== null) {
21467
+ relatedIds.add(match[1]);
21468
+ }
21469
+ const significantTags = tags.filter(
21470
+ (t) => !t.startsWith("sprint:") && !t.startsWith("focus:") && t !== "risk"
21471
+ );
21472
+ if (significantTags.length > 0) {
21473
+ for (const d of allDocs) {
21474
+ if (d.frontmatter.id === risk.id) continue;
21475
+ const dTags = d.frontmatter.tags ?? [];
21476
+ if (significantTags.some((t) => dTags.includes(t))) {
21477
+ relatedIds.add(d.frontmatter.id);
21478
+ }
21479
+ }
21480
+ }
21481
+ const about = doc.frontmatter.aboutArtifact;
21482
+ if (about) {
21483
+ relatedIds.add(about);
21484
+ for (const d of allDocs) {
21485
+ if (d.frontmatter.aboutArtifact === about && d.frontmatter.id !== risk.id) {
21486
+ relatedIds.add(d.frontmatter.id);
21487
+ }
21488
+ }
21489
+ }
21490
+ const relatedDocs = [...relatedIds].map((id) => store.get(id)).filter((d) => d != null).slice(0, 20);
21491
+ if (relatedDocs.length > 0) {
21492
+ sections.push(`
21493
+ ## Related Documents (${relatedDocs.length})`);
21494
+ for (const rd of relatedDocs) {
21495
+ const owner = rd.frontmatter.owner ?? "unowned";
21496
+ const summary = rd.content.trim().slice(0, 300);
21497
+ sections.push(
21498
+ `- ${rd.frontmatter.id} (${rd.frontmatter.type}) [${rd.frontmatter.status}] \u2014 ${rd.frontmatter.title}`
21499
+ );
21500
+ sections.push(` Owner: ${owner}${rd.frontmatter.dueDate ? `, Due: ${rd.frontmatter.dueDate}` : ""}`);
21501
+ if (summary) sections.push(` Summary: ${summary}${rd.content.trim().length > 300 ? "..." : ""}`);
21502
+ }
21503
+ }
21504
+ }
21505
+ sections.push("");
21506
+ sections.push(`---`);
21507
+ sections.push(`
21508
+ Generate the risk assessment for ${risk.id} based on the data above.`);
21509
+ return sections.join("\n");
21510
+ }
21511
+
21311
21512
  // src/web/templates/persona-switcher.ts
21312
21513
  function renderPersonaSwitcher(current, _currentPath) {
21313
21514
  const views = getAllPersonaViews();
@@ -22567,11 +22768,16 @@ function sprintSummaryPage(data, cached2) {
22567
22768
  <div class="card-value">${data.linkedEpics.length}</div>
22568
22769
  <div class="card-sub">linked to sprint</div>
22569
22770
  </div>
22570
- <div class="card">
22771
+ <a class="card card-link" href="sprint-blockers">
22571
22772
  <div class="card-label">Blockers</div>
22572
22773
  <div class="card-value${data.blockers.length > 0 ? " priority-high" : ""}">${data.blockers.length}</div>
22573
- <div class="card-sub">${data.openActions.length} open actions</div>
22574
- </div>
22774
+ <div class="card-sub">${data.workItems.blocked} blocked items</div>
22775
+ </a>
22776
+ <a class="card card-link" href="sprint-risks">
22777
+ <div class="card-label">Risks</div>
22778
+ <div class="card-value${data.risks.length > 0 ? " priority-medium" : ""}">${data.risks.length}</div>
22779
+ <div class="card-sub">open risk items</div>
22780
+ </a>
22575
22781
  </div>`;
22576
22782
  const epicsTable = data.linkedEpics.length > 0 ? collapsibleSection(
22577
22783
  "ss-epics",
@@ -23961,6 +24167,168 @@ function upcomingPage(data) {
23961
24167
  `;
23962
24168
  }
23963
24169
 
24170
+ // src/web/templates/pages/sprint-blockers.ts
24171
+ function sprintBlockersPage(data, store) {
24172
+ if (!data) {
24173
+ return `
24174
+ <div class="page-header">
24175
+ <h2>Sprint Blockers</h2>
24176
+ <div class="subtitle">Blocked items in the active sprint</div>
24177
+ </div>
24178
+ <div class="empty">
24179
+ <h3>No Active Sprint</h3>
24180
+ <p>No active sprint found.</p>
24181
+ </div>`;
24182
+ }
24183
+ const blockerDocs = data.blockers.map((b) => {
24184
+ const doc = store.get(b.id);
24185
+ return { ...b, doc };
24186
+ });
24187
+ const statsCards = `
24188
+ <div class="cards">
24189
+ <div class="card">
24190
+ <div class="card-label">Blocked Items</div>
24191
+ <div class="card-value${blockerDocs.length > 0 ? " priority-high" : ""}">${blockerDocs.length}</div>
24192
+ <div class="card-sub">in ${escapeHtml(data.sprint.id)}</div>
24193
+ </div>
24194
+ </div>`;
24195
+ const itemCards = blockerDocs.map((b) => {
24196
+ const doc = b.doc;
24197
+ const owner = doc?.frontmatter.owner;
24198
+ const assignee = doc?.frontmatter.assignee;
24199
+ const content = doc?.content?.trim();
24200
+ return `
24201
+ <div class="blocker-card">
24202
+ <div class="blocker-card-header">
24203
+ <a href="/docs/${escapeHtml(b.type)}/${escapeHtml(b.id)}">${escapeHtml(b.id)}</a>
24204
+ <span class="text-dim">${escapeHtml(typeLabel(b.type))}</span>
24205
+ ${statusBadge("blocked")}
24206
+ </div>
24207
+ <h4 class="blocker-card-title">${escapeHtml(b.title)}</h4>
24208
+ <div class="blocker-card-meta">
24209
+ ${owner ? `<span><strong>Owner:</strong> ${escapeHtml(owner)}</span>` : ""}
24210
+ ${assignee ? `<span><strong>Assignee:</strong> ${escapeHtml(assignee)}</span>` : ""}
24211
+ ${doc?.frontmatter.created ? `<span><strong>Created:</strong> ${formatDate(doc.frontmatter.created)}</span>` : ""}
24212
+ </div>
24213
+ ${content ? `<div class="blocker-card-content detail-content">${renderMarkdown(content)}</div>` : ""}
24214
+ </div>`;
24215
+ }).join("");
24216
+ const emptyMessage = blockerDocs.length === 0 ? `<div class="empty"><h3>No Blockers</h3><p>No blocked items in this sprint.</p></div>` : "";
24217
+ return `
24218
+ <div class="page-header">
24219
+ <h2>Sprint Blockers</h2>
24220
+ <div class="subtitle">Blocked items in ${escapeHtml(data.sprint.id)} \u2014 ${escapeHtml(data.sprint.title)}</div>
24221
+ </div>
24222
+ ${statsCards}
24223
+ ${emptyMessage}
24224
+ ${itemCards}`;
24225
+ }
24226
+
24227
+ // src/web/templates/pages/sprint-risks.ts
24228
+ function sprintRisksPage(data, store) {
24229
+ if (!data) {
24230
+ return `
24231
+ <div class="page-header">
24232
+ <h2>Sprint Risks</h2>
24233
+ <div class="subtitle">Risk items in the active sprint</div>
24234
+ </div>
24235
+ <div class="empty">
24236
+ <h3>No Active Sprint</h3>
24237
+ <p>No active sprint found.</p>
24238
+ </div>`;
24239
+ }
24240
+ const riskDocs = data.risks.map((r) => {
24241
+ const doc = store.get(r.id);
24242
+ return { ...r, doc };
24243
+ });
24244
+ const statsCards = `
24245
+ <div class="cards">
24246
+ <div class="card">
24247
+ <div class="card-label">Open Risks</div>
24248
+ <div class="card-value${riskDocs.length > 0 ? " priority-medium" : ""}">${riskDocs.length}</div>
24249
+ <div class="card-sub">in ${escapeHtml(data.sprint.id)}</div>
24250
+ </div>
24251
+ </div>`;
24252
+ const itemCards = riskDocs.map((r) => {
24253
+ const doc = r.doc;
24254
+ const owner = doc?.frontmatter.owner;
24255
+ const assignee = doc?.frontmatter.assignee;
24256
+ const content = doc?.content?.trim();
24257
+ return `
24258
+ <div class="blocker-card" id="risk-${escapeHtml(r.id)}">
24259
+ <div class="blocker-card-header">
24260
+ <a href="/docs/${escapeHtml(r.type)}/${escapeHtml(r.id)}">${escapeHtml(r.id)}</a>
24261
+ <span class="text-dim">${escapeHtml(typeLabel(r.type))}</span>
24262
+ ${statusBadge(doc?.frontmatter.status ?? "open")}
24263
+ </div>
24264
+ <h4 class="blocker-card-title">${escapeHtml(r.title)}</h4>
24265
+ <div class="blocker-card-meta">
24266
+ ${owner ? `<span><strong>Owner:</strong> ${escapeHtml(owner)}</span>` : ""}
24267
+ ${assignee ? `<span><strong>Assignee:</strong> ${escapeHtml(assignee)}</span>` : ""}
24268
+ ${doc?.frontmatter.created ? `<span><strong>Created:</strong> ${formatDate(doc.frontmatter.created)}</span>` : ""}
24269
+ </div>
24270
+ ${content ? `<div class="blocker-card-content detail-content">${renderMarkdown(content)}</div>` : ""}
24271
+ <div class="risk-assessment" id="assessment-${escapeHtml(r.id)}">
24272
+ <button class="sprint-generate-btn risk-assess-btn" onclick="generateAssessment('${escapeHtml(r.id)}', this)">Assess Risk</button>
24273
+ <div class="sprint-loading risk-assess-loading" style="display:none">
24274
+ <div class="sprint-spinner"></div>
24275
+ <span>Analyzing...</span>
24276
+ </div>
24277
+ <div class="sprint-error risk-assess-error" style="display:none"></div>
24278
+ <div class="risk-assessment-content detail-content" style="display:none"></div>
24279
+ </div>
24280
+ </div>`;
24281
+ }).join("");
24282
+ const emptyMessage = riskDocs.length === 0 ? `<div class="empty"><h3>No Risks</h3><p>No open risk items in this sprint.</p></div>` : "";
24283
+ const script = riskDocs.length > 0 ? `
24284
+ <script>
24285
+ async function generateAssessment(riskId, btn) {
24286
+ var container = document.getElementById('assessment-' + riskId);
24287
+ var loading = container.querySelector('.risk-assess-loading');
24288
+ var errorEl = container.querySelector('.risk-assess-error');
24289
+ var contentEl = container.querySelector('.risk-assessment-content');
24290
+
24291
+ btn.disabled = true;
24292
+ btn.style.display = 'none';
24293
+ loading.style.display = 'flex';
24294
+ errorEl.style.display = 'none';
24295
+
24296
+ try {
24297
+ var res = await fetch('/api/risk-assessment', {
24298
+ method: 'POST',
24299
+ headers: { 'Content-Type': 'application/json' },
24300
+ body: JSON.stringify({ sprintId: '${escapeHtml(data.sprint.id)}', riskId: riskId })
24301
+ });
24302
+ var json = await res.json();
24303
+ if (!res.ok) throw new Error(json.error || 'Failed to generate assessment');
24304
+
24305
+ contentEl.innerHTML = '<div class="risk-assessment-label">AI Assessment</div>' + json.html;
24306
+ contentEl.style.display = 'block';
24307
+
24308
+ loading.style.display = 'none';
24309
+ btn.textContent = 'Regenerate';
24310
+ btn.style.display = '';
24311
+ btn.disabled = false;
24312
+ } catch (e) {
24313
+ loading.style.display = 'none';
24314
+ errorEl.textContent = e.message;
24315
+ errorEl.style.display = 'block';
24316
+ btn.style.display = '';
24317
+ btn.disabled = false;
24318
+ }
24319
+ }
24320
+ </script>` : "";
24321
+ return `
24322
+ <div class="page-header">
24323
+ <h2>Sprint Risks</h2>
24324
+ <div class="subtitle">Risk items in ${escapeHtml(data.sprint.id)} \u2014 ${escapeHtml(data.sprint.title)}</div>
24325
+ </div>
24326
+ ${statsCards}
24327
+ ${emptyMessage}
24328
+ ${itemCards}
24329
+ ${script}`;
24330
+ }
24331
+
23964
24332
  // src/web/templates/pages/shared-wrappers.ts
23965
24333
  function sharedTimelinePage(ctx) {
23966
24334
  const diagrams = getDiagramData(ctx.store);
@@ -23990,6 +24358,16 @@ function sharedSprintSummaryPage(ctx) {
23990
24358
  const data = getSprintSummaryData(ctx.store, sprintId);
23991
24359
  return sprintSummaryPage(data);
23992
24360
  }
24361
+ function sharedSprintBlockersPage(ctx) {
24362
+ const sprintId = ctx.searchParams?.get("sprint") ?? void 0;
24363
+ const data = getSprintSummaryData(ctx.store, sprintId);
24364
+ return sprintBlockersPage(data, ctx.store);
24365
+ }
24366
+ function sharedSprintRisksPage(ctx) {
24367
+ const sprintId = ctx.searchParams?.get("sprint") ?? void 0;
24368
+ const data = getSprintSummaryData(ctx.store, sprintId);
24369
+ return sprintRisksPage(data, ctx.store);
24370
+ }
23993
24371
 
23994
24372
  // src/web/shared-page-registration.ts
23995
24373
  var SHARED_PAGES = [
@@ -23998,7 +24376,9 @@ var SHARED_PAGES = [
23998
24376
  { pageId: "upcoming", renderer: sharedUpcomingPage },
23999
24377
  { pageId: "gar", renderer: sharedGarPage },
24000
24378
  { pageId: "health", renderer: sharedHealthPage },
24001
- { pageId: "sprint-summary", renderer: sharedSprintSummaryPage }
24379
+ { pageId: "sprint-summary", renderer: sharedSprintSummaryPage },
24380
+ { pageId: "sprint-blockers", renderer: sharedSprintBlockersPage },
24381
+ { pageId: "sprint-risks", renderer: sharedSprintRisksPage }
24002
24382
  ];
24003
24383
  for (const persona of ["po", "dm", "tl"]) {
24004
24384
  for (const { pageId, renderer } of SHARED_PAGES) {
@@ -24199,6 +24579,37 @@ function handleRequest(req, res, store, projectName, navGroups) {
24199
24579
  });
24200
24580
  return;
24201
24581
  }
24582
+ if (pathname === "/api/risk-assessment" && req.method === "POST") {
24583
+ let bodyStr = "";
24584
+ req.on("data", (chunk) => {
24585
+ bodyStr += chunk;
24586
+ });
24587
+ req.on("end", async () => {
24588
+ try {
24589
+ const { sprintId, riskId } = JSON.parse(bodyStr || "{}");
24590
+ if (!riskId) {
24591
+ res.writeHead(400, { "Content-Type": "application/json" });
24592
+ res.end(JSON.stringify({ error: "riskId is required" }));
24593
+ return;
24594
+ }
24595
+ const data = getSprintSummaryData(store, sprintId);
24596
+ if (!data) {
24597
+ res.writeHead(404, { "Content-Type": "application/json" });
24598
+ res.end(JSON.stringify({ error: "Sprint not found" }));
24599
+ return;
24600
+ }
24601
+ const markdown = await generateRiskAssessment(data, riskId, store);
24602
+ const html = renderMarkdown(markdown);
24603
+ res.writeHead(200, { "Content-Type": "application/json" });
24604
+ res.end(JSON.stringify({ riskId, html }));
24605
+ } catch (err) {
24606
+ console.error("[marvin web] Risk assessment generation error:", err);
24607
+ res.writeHead(500, { "Content-Type": "application/json" });
24608
+ res.end(JSON.stringify({ error: "Failed to generate risk assessment" }));
24609
+ }
24610
+ });
24611
+ return;
24612
+ }
24202
24613
  const detailMatch = pathname.match(/^\/docs\/([^/]+)\/([^/]+)$/);
24203
24614
  if (detailMatch) {
24204
24615
  const [, type, id] = detailMatch;
@@ -26281,11 +26692,11 @@ function createMarvinMcpServer(store, options) {
26281
26692
  }
26282
26693
 
26283
26694
  // src/agent/session-namer.ts
26284
- import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
26695
+ import { query as query3 } from "@anthropic-ai/claude-agent-sdk";
26285
26696
  async function generateSessionName(turns) {
26286
26697
  try {
26287
26698
  const transcript = turns.slice(-20).map((t) => `${t.role}: ${t.content.slice(0, 200)}`).join("\n");
26288
- const result = query2({
26699
+ const result = query3({
26289
26700
  prompt: `Summarize this conversation in 3-5 words as a kebab-case name suitable for a filename. Output ONLY the name, nothing else.
26290
26701
 
26291
26702
  ${transcript}`,
@@ -26563,7 +26974,7 @@ Marvin \u2014 ${persona.name}
26563
26974
  if (existingSession) {
26564
26975
  queryOptions.resume = existingSession.id;
26565
26976
  }
26566
- const conversation = query3({
26977
+ const conversation = query4({
26567
26978
  prompt,
26568
26979
  options: queryOptions
26569
26980
  });
@@ -26922,7 +27333,7 @@ import * as fs12 from "fs";
26922
27333
  import * as path12 from "path";
26923
27334
  import chalk7 from "chalk";
26924
27335
  import ora2 from "ora";
26925
- import { query as query4 } from "@anthropic-ai/claude-agent-sdk";
27336
+ import { query as query5 } from "@anthropic-ai/claude-agent-sdk";
26926
27337
 
26927
27338
  // src/sources/prompts.ts
26928
27339
  function buildIngestSystemPrompt(persona, projectConfig, isDraft) {
@@ -27055,7 +27466,7 @@ async function ingestFile(options) {
27055
27466
  const spinner = ora2({ text: `Analyzing ${fileName}...`, color: "cyan" });
27056
27467
  spinner.start();
27057
27468
  try {
27058
- const conversation = query4({
27469
+ const conversation = query5({
27059
27470
  prompt: userPrompt,
27060
27471
  options: {
27061
27472
  systemPrompt,
@@ -27585,7 +27996,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
27585
27996
  import { tool as tool24 } from "@anthropic-ai/claude-agent-sdk";
27586
27997
 
27587
27998
  // src/skills/action-runner.ts
27588
- import { query as query5 } from "@anthropic-ai/claude-agent-sdk";
27999
+ import { query as query6 } from "@anthropic-ai/claude-agent-sdk";
27589
28000
  var GOVERNANCE_TOOL_NAMES2 = [
27590
28001
  "mcp__marvin-governance__list_decisions",
27591
28002
  "mcp__marvin-governance__get_decision",
@@ -27607,7 +28018,7 @@ async function runSkillAction(action, userPrompt, context) {
27607
28018
  try {
27608
28019
  const mcpServer = createMarvinMcpServer(context.store);
27609
28020
  const allowedTools = action.allowGovernanceTools !== false ? GOVERNANCE_TOOL_NAMES2 : [];
27610
- const conversation = query5({
28021
+ const conversation = query6({
27611
28022
  prompt: userPrompt,
27612
28023
  options: {
27613
28024
  systemPrompt: action.systemPrompt,
@@ -28648,7 +29059,7 @@ import chalk13 from "chalk";
28648
29059
  // src/analysis/analyze.ts
28649
29060
  import chalk12 from "chalk";
28650
29061
  import ora4 from "ora";
28651
- import { query as query6 } from "@anthropic-ai/claude-agent-sdk";
29062
+ import { query as query7 } from "@anthropic-ai/claude-agent-sdk";
28652
29063
 
28653
29064
  // src/analysis/prompts.ts
28654
29065
  function buildAnalyzeSystemPrompt(persona, projectConfig, isDraft) {
@@ -28778,7 +29189,7 @@ async function analyzeMeeting(options) {
28778
29189
  const spinner = ora4({ text: `Analyzing meeting ${meetingId}...`, color: "cyan" });
28779
29190
  spinner.start();
28780
29191
  try {
28781
- const conversation = query6({
29192
+ const conversation = query7({
28782
29193
  prompt: userPrompt,
28783
29194
  options: {
28784
29195
  systemPrompt,
@@ -28905,7 +29316,7 @@ import chalk15 from "chalk";
28905
29316
  // src/contributions/contribute.ts
28906
29317
  import chalk14 from "chalk";
28907
29318
  import ora5 from "ora";
28908
- import { query as query7 } from "@anthropic-ai/claude-agent-sdk";
29319
+ import { query as query8 } from "@anthropic-ai/claude-agent-sdk";
28909
29320
 
28910
29321
  // src/contributions/prompts.ts
28911
29322
  function buildContributeSystemPrompt(persona, contributionType, projectConfig, isDraft) {
@@ -29159,7 +29570,7 @@ async function contributeFromPersona(options) {
29159
29570
  "mcp__marvin-governance__get_action",
29160
29571
  "mcp__marvin-governance__get_question"
29161
29572
  ];
29162
- const conversation = query7({
29573
+ const conversation = query8({
29163
29574
  prompt: userPrompt,
29164
29575
  options: {
29165
29576
  systemPrompt,
@@ -29655,7 +30066,7 @@ function createProgram() {
29655
30066
  const program2 = new Command();
29656
30067
  program2.name("marvin").description(
29657
30068
  "AI-powered product development assistant with Product Owner, Delivery Manager, and Technical Lead personas"
29658
- ).version("0.5.3");
30069
+ ).version("0.5.5");
29659
30070
  program2.command("init").description("Initialize a new Marvin project in the current directory").action(async () => {
29660
30071
  await initCommand();
29661
30072
  });