mrvn-cli 0.4.6 → 0.4.7

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(query7) {
226
+ list(query8) {
227
227
  const results = [];
228
- const types = query7?.type ? [query7.type] : Object.keys(this.typeDirs);
228
+ const types = query8?.type ? [query8.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 (query7?.status && doc.frontmatter.status !== query7.status) continue;
240
- if (query7?.owner && doc.frontmatter.owner !== query7.owner) continue;
241
- if (query7?.tag && (!doc.frontmatter.tags || !doc.frontmatter.tags.includes(query7.tag)))
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)))
242
242
  continue;
243
243
  results.push(doc);
244
244
  }
@@ -15412,6 +15412,210 @@ function evaluateHealth(projectName, metrics) {
15412
15412
  };
15413
15413
  }
15414
15414
 
15415
+ // src/reports/sprint-summary/collector.ts
15416
+ var DONE_STATUSES = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
15417
+ function collectSprintSummaryData(store, sprintId) {
15418
+ const allDocs = store.list();
15419
+ const sprintDocs = allDocs.filter((d) => d.frontmatter.type === "sprint");
15420
+ let sprintDoc;
15421
+ if (sprintId) {
15422
+ sprintDoc = sprintDocs.find((d) => d.frontmatter.id === sprintId);
15423
+ } else {
15424
+ sprintDoc = sprintDocs.find((d) => d.frontmatter.status === "active");
15425
+ }
15426
+ if (!sprintDoc) return null;
15427
+ const fm = sprintDoc.frontmatter;
15428
+ const startDate = fm.startDate;
15429
+ const endDate = fm.endDate;
15430
+ const today = /* @__PURE__ */ new Date();
15431
+ const todayStr = today.toISOString().slice(0, 10);
15432
+ let daysElapsed = 0;
15433
+ let daysRemaining = 0;
15434
+ let totalDays = 0;
15435
+ let percentComplete = 0;
15436
+ if (startDate && endDate) {
15437
+ const startMs = new Date(startDate).getTime();
15438
+ const endMs = new Date(endDate).getTime();
15439
+ const todayMs = today.getTime();
15440
+ const msPerDay = 864e5;
15441
+ totalDays = Math.max(1, Math.round((endMs - startMs) / msPerDay));
15442
+ daysElapsed = Math.max(0, Math.round((todayMs - startMs) / msPerDay));
15443
+ daysRemaining = Math.max(0, Math.round((endMs - todayMs) / msPerDay));
15444
+ percentComplete = Math.min(100, Math.round(daysElapsed / totalDays * 100));
15445
+ }
15446
+ const linkedEpicIds = normalizeLinkedEpics(fm.linkedEpics);
15447
+ const epicToTasks = /* @__PURE__ */ new Map();
15448
+ const allTasks = allDocs.filter((d) => d.frontmatter.type === "task");
15449
+ for (const task of allTasks) {
15450
+ const tags = task.frontmatter.tags ?? [];
15451
+ for (const tag of tags) {
15452
+ if (tag.startsWith("epic:")) {
15453
+ const epicId = tag.slice(5);
15454
+ if (!epicToTasks.has(epicId)) epicToTasks.set(epicId, []);
15455
+ epicToTasks.get(epicId).push(task);
15456
+ }
15457
+ }
15458
+ }
15459
+ const linkedEpics = linkedEpicIds.map((epicId) => {
15460
+ const epic = store.get(epicId);
15461
+ const tasks = epicToTasks.get(epicId) ?? [];
15462
+ const tasksDone = tasks.filter((t) => DONE_STATUSES.has(t.frontmatter.status)).length;
15463
+ return {
15464
+ id: epicId,
15465
+ title: epic?.frontmatter.title ?? "(not found)",
15466
+ status: epic?.frontmatter.status ?? "unknown",
15467
+ tasksDone,
15468
+ tasksTotal: tasks.length
15469
+ };
15470
+ });
15471
+ const sprintTag = `sprint:${fm.id}`;
15472
+ const workItemDocs = allDocs.filter(
15473
+ (d) => d.frontmatter.type !== "sprint" && d.frontmatter.type !== "epic" && d.frontmatter.tags?.includes(sprintTag)
15474
+ );
15475
+ const byStatus = {};
15476
+ const byType = {};
15477
+ let doneCount = 0;
15478
+ let inProgressCount = 0;
15479
+ let openCount = 0;
15480
+ let blockedCount = 0;
15481
+ for (const doc of workItemDocs) {
15482
+ const s = doc.frontmatter.status;
15483
+ byStatus[s] = (byStatus[s] ?? 0) + 1;
15484
+ byType[doc.frontmatter.type] = (byType[doc.frontmatter.type] ?? 0) + 1;
15485
+ if (DONE_STATUSES.has(s)) doneCount++;
15486
+ else if (s === "in-progress") inProgressCount++;
15487
+ else if (s === "blocked") blockedCount++;
15488
+ else openCount++;
15489
+ }
15490
+ const workItems = {
15491
+ total: workItemDocs.length,
15492
+ done: doneCount,
15493
+ inProgress: inProgressCount,
15494
+ open: openCount,
15495
+ blocked: blockedCount,
15496
+ completionPct: workItemDocs.length > 0 ? Math.round(doneCount / workItemDocs.length * 100) : 0,
15497
+ byStatus,
15498
+ byType,
15499
+ items: workItemDocs.map((d) => ({
15500
+ id: d.frontmatter.id,
15501
+ title: d.frontmatter.title,
15502
+ type: d.frontmatter.type,
15503
+ status: d.frontmatter.status
15504
+ }))
15505
+ };
15506
+ const meetings = [];
15507
+ if (startDate && endDate) {
15508
+ const meetingDocs = allDocs.filter((d) => d.frontmatter.type === "meeting");
15509
+ for (const m of meetingDocs) {
15510
+ const meetingDate = m.frontmatter.date ?? m.frontmatter.created.slice(0, 10);
15511
+ if (meetingDate >= startDate && meetingDate <= endDate) {
15512
+ meetings.push({
15513
+ id: m.frontmatter.id,
15514
+ title: m.frontmatter.title,
15515
+ date: meetingDate
15516
+ });
15517
+ }
15518
+ }
15519
+ meetings.sort((a, b) => a.date.localeCompare(b.date));
15520
+ }
15521
+ const artifacts = [];
15522
+ if (startDate && endDate) {
15523
+ for (const doc of allDocs) {
15524
+ if (doc.frontmatter.type === "sprint") continue;
15525
+ const created = doc.frontmatter.created.slice(0, 10);
15526
+ const updated = doc.frontmatter.updated.slice(0, 10);
15527
+ if (created >= startDate && created <= endDate) {
15528
+ artifacts.push({
15529
+ id: doc.frontmatter.id,
15530
+ title: doc.frontmatter.title,
15531
+ type: doc.frontmatter.type,
15532
+ action: "created",
15533
+ date: created
15534
+ });
15535
+ } else if (updated >= startDate && updated <= endDate && updated !== created) {
15536
+ artifacts.push({
15537
+ id: doc.frontmatter.id,
15538
+ title: doc.frontmatter.title,
15539
+ type: doc.frontmatter.type,
15540
+ action: "updated",
15541
+ date: updated
15542
+ });
15543
+ }
15544
+ }
15545
+ artifacts.sort((a, b) => b.date.localeCompare(a.date));
15546
+ }
15547
+ const relevantTags = /* @__PURE__ */ new Set([sprintTag, ...linkedEpicIds.map((id) => `epic:${id}`)]);
15548
+ const openActions = allDocs.filter(
15549
+ (d) => d.frontmatter.type === "action" && !DONE_STATUSES.has(d.frontmatter.status) && d.frontmatter.tags?.some((t) => relevantTags.has(t))
15550
+ ).map((d) => ({
15551
+ id: d.frontmatter.id,
15552
+ title: d.frontmatter.title,
15553
+ owner: d.frontmatter.owner,
15554
+ dueDate: d.frontmatter.dueDate
15555
+ }));
15556
+ const openQuestions = allDocs.filter(
15557
+ (d) => d.frontmatter.type === "question" && d.frontmatter.status === "open" && d.frontmatter.tags?.some((t) => relevantTags.has(t))
15558
+ ).map((d) => ({
15559
+ id: d.frontmatter.id,
15560
+ title: d.frontmatter.title
15561
+ }));
15562
+ const blockers = allDocs.filter(
15563
+ (d) => d.frontmatter.status === "blocked" && d.frontmatter.tags?.includes(sprintTag)
15564
+ ).map((d) => ({
15565
+ id: d.frontmatter.id,
15566
+ title: d.frontmatter.title,
15567
+ type: d.frontmatter.type
15568
+ }));
15569
+ const riskBlockers = allDocs.filter(
15570
+ (d) => !DONE_STATUSES.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)
15571
+ );
15572
+ for (const d of riskBlockers) {
15573
+ blockers.push({
15574
+ id: d.frontmatter.id,
15575
+ title: d.frontmatter.title,
15576
+ type: d.frontmatter.type
15577
+ });
15578
+ }
15579
+ let velocity = null;
15580
+ const currentRate = workItems.completionPct;
15581
+ const completedSprints = sprintDocs.filter((s) => DONE_STATUSES.has(s.frontmatter.status) && s.frontmatter.id !== fm.id).sort((a, b) => (b.frontmatter.endDate ?? "").localeCompare(a.frontmatter.endDate ?? ""));
15582
+ if (completedSprints.length > 0) {
15583
+ const prev = completedSprints[0];
15584
+ const prevTag = `sprint:${prev.frontmatter.id}`;
15585
+ const prevWorkItems = allDocs.filter(
15586
+ (d) => d.frontmatter.type !== "sprint" && d.frontmatter.type !== "epic" && d.frontmatter.tags?.includes(prevTag)
15587
+ );
15588
+ const prevDone = prevWorkItems.filter((d) => DONE_STATUSES.has(d.frontmatter.status)).length;
15589
+ const prevRate = prevWorkItems.length > 0 ? Math.round(prevDone / prevWorkItems.length * 100) : 0;
15590
+ velocity = {
15591
+ currentCompletionRate: currentRate,
15592
+ previousSprintRate: prevRate,
15593
+ previousSprintId: prev.frontmatter.id
15594
+ };
15595
+ } else {
15596
+ velocity = { currentCompletionRate: currentRate };
15597
+ }
15598
+ return {
15599
+ sprint: {
15600
+ id: fm.id,
15601
+ title: fm.title,
15602
+ goal: fm.goal,
15603
+ status: fm.status,
15604
+ startDate,
15605
+ endDate
15606
+ },
15607
+ timeline: { daysElapsed, daysRemaining, totalDays, percentComplete },
15608
+ linkedEpics,
15609
+ workItems,
15610
+ meetings,
15611
+ artifacts,
15612
+ openActions,
15613
+ openQuestions,
15614
+ blockers,
15615
+ velocity
15616
+ };
15617
+ }
15618
+
15415
15619
  // src/web/data.ts
15416
15620
  function getOverviewData(store) {
15417
15621
  const types = [];
@@ -15533,7 +15737,7 @@ function computeUrgency(dueDateStr, todayStr) {
15533
15737
  if (diffDays <= 14) return "upcoming";
15534
15738
  return "later";
15535
15739
  }
15536
- var DONE_STATUSES = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
15740
+ var DONE_STATUSES2 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
15537
15741
  function getUpcomingData(store) {
15538
15742
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
15539
15743
  const allDocs = store.list();
@@ -15542,7 +15746,7 @@ function getUpcomingData(store) {
15542
15746
  docById.set(doc.frontmatter.id, doc);
15543
15747
  }
15544
15748
  const actions = allDocs.filter(
15545
- (d) => d.frontmatter.type === "action" && !DONE_STATUSES.has(d.frontmatter.status)
15749
+ (d) => d.frontmatter.type === "action" && !DONE_STATUSES2.has(d.frontmatter.status)
15546
15750
  );
15547
15751
  const actionsWithDue = actions.filter((d) => d.frontmatter.dueDate);
15548
15752
  const sprints = allDocs.filter((d) => d.frontmatter.type === "sprint");
@@ -15606,7 +15810,7 @@ function getUpcomingData(store) {
15606
15810
  const sprintEnd = sprint.frontmatter.endDate;
15607
15811
  const sprintTaskDocs = getSprintTasks(sprint);
15608
15812
  for (const task of sprintTaskDocs) {
15609
- if (DONE_STATUSES.has(task.frontmatter.status)) continue;
15813
+ if (DONE_STATUSES2.has(task.frontmatter.status)) continue;
15610
15814
  const existing = taskSprintMap.get(task.frontmatter.id);
15611
15815
  if (!existing || sprintEnd < existing.sprintEnd) {
15612
15816
  taskSprintMap.set(task.frontmatter.id, { task, sprint, sprintEnd });
@@ -15623,7 +15827,7 @@ function getUpcomingData(store) {
15623
15827
  urgency: computeUrgency(sprintEnd, today)
15624
15828
  })).sort((a, b) => a.sprintEndDate.localeCompare(b.sprintEndDate));
15625
15829
  const openItems = allDocs.filter(
15626
- (d) => ["action", "question", "task"].includes(d.frontmatter.type) && !DONE_STATUSES.has(d.frontmatter.status)
15830
+ (d) => ["action", "question", "task"].includes(d.frontmatter.type) && !DONE_STATUSES2.has(d.frontmatter.status)
15627
15831
  );
15628
15832
  const fourteenDaysAgo = new Date(todayMs - fourteenDaysMs).toISOString().slice(0, 10);
15629
15833
  const recentMeetings = allDocs.filter(
@@ -15721,6 +15925,9 @@ function getUpcomingData(store) {
15721
15925
  }).filter((item) => item.score > 0).sort((a, b) => b.score - a.score).slice(0, 15);
15722
15926
  return { dueSoonActions, dueSoonSprintTasks, trending };
15723
15927
  }
15928
+ function getSprintSummaryData(store, sprintId) {
15929
+ return collectSprintSummaryData(store, sprintId);
15930
+ }
15724
15931
 
15725
15932
  // src/web/templates/layout.ts
15726
15933
  function collapsibleSection(sectionId, title, content, opts) {
@@ -15862,6 +16069,7 @@ function layout(opts, body) {
15862
16069
  const topItems = [
15863
16070
  { href: "/", label: "Overview" },
15864
16071
  { href: "/upcoming", label: "Upcoming" },
16072
+ { href: "/sprint-summary", label: "Sprint Summary" },
15865
16073
  { href: "/timeline", label: "Timeline" },
15866
16074
  { href: "/board", label: "Board" },
15867
16075
  { href: "/gar", label: "GAR Report" },
@@ -16872,6 +17080,112 @@ tr:hover td {
16872
17080
 
16873
17081
  .text-dim { color: var(--text-dim); }
16874
17082
 
17083
+ /* Sprint Summary */
17084
+ .sprint-goal {
17085
+ background: var(--bg-card);
17086
+ border: 1px solid var(--border);
17087
+ border-radius: var(--radius);
17088
+ padding: 0.75rem 1rem;
17089
+ margin-bottom: 1rem;
17090
+ font-size: 0.9rem;
17091
+ color: var(--text);
17092
+ }
17093
+
17094
+ .sprint-progress-bar {
17095
+ position: relative;
17096
+ height: 24px;
17097
+ background: var(--bg-card);
17098
+ border: 1px solid var(--border);
17099
+ border-radius: 12px;
17100
+ margin-bottom: 1.25rem;
17101
+ overflow: hidden;
17102
+ }
17103
+
17104
+ .sprint-progress-fill {
17105
+ height: 100%;
17106
+ background: linear-gradient(90deg, var(--accent-dim), var(--accent));
17107
+ border-radius: 12px;
17108
+ transition: width 0.3s ease;
17109
+ }
17110
+
17111
+ .sprint-progress-label {
17112
+ position: absolute;
17113
+ top: 50%;
17114
+ left: 50%;
17115
+ transform: translate(-50%, -50%);
17116
+ font-size: 0.7rem;
17117
+ font-weight: 700;
17118
+ color: var(--text);
17119
+ }
17120
+
17121
+ .sprint-ai-section {
17122
+ margin-top: 2rem;
17123
+ background: var(--bg-card);
17124
+ border: 1px solid var(--border);
17125
+ border-radius: var(--radius);
17126
+ padding: 1.5rem;
17127
+ }
17128
+
17129
+ .sprint-ai-section h3 {
17130
+ font-size: 1rem;
17131
+ font-weight: 600;
17132
+ margin-bottom: 0.5rem;
17133
+ }
17134
+
17135
+ .sprint-generate-btn {
17136
+ background: var(--accent);
17137
+ color: #fff;
17138
+ border: none;
17139
+ border-radius: var(--radius);
17140
+ padding: 0.5rem 1.25rem;
17141
+ font-size: 0.85rem;
17142
+ font-weight: 600;
17143
+ cursor: pointer;
17144
+ margin-top: 0.75rem;
17145
+ transition: background 0.15s;
17146
+ }
17147
+
17148
+ .sprint-generate-btn:hover:not(:disabled) {
17149
+ background: var(--accent-dim);
17150
+ }
17151
+
17152
+ .sprint-generate-btn:disabled {
17153
+ opacity: 0.5;
17154
+ cursor: not-allowed;
17155
+ }
17156
+
17157
+ .sprint-loading {
17158
+ display: flex;
17159
+ align-items: center;
17160
+ gap: 0.75rem;
17161
+ padding: 1rem 0;
17162
+ color: var(--text-dim);
17163
+ font-size: 0.85rem;
17164
+ }
17165
+
17166
+ .sprint-spinner {
17167
+ width: 20px;
17168
+ height: 20px;
17169
+ border: 2px solid var(--border);
17170
+ border-top-color: var(--accent);
17171
+ border-radius: 50%;
17172
+ animation: sprint-spin 0.8s linear infinite;
17173
+ }
17174
+
17175
+ @keyframes sprint-spin {
17176
+ to { transform: rotate(360deg); }
17177
+ }
17178
+
17179
+ .sprint-error {
17180
+ color: var(--red);
17181
+ font-size: 0.85rem;
17182
+ padding: 0.5rem 0;
17183
+ }
17184
+
17185
+ .sprint-ai-section .detail-content {
17186
+ margin-top: 1rem;
17187
+ }
17188
+
16875
17189
  /* Collapsible sections */
16876
17190
  .collapsible-header {
16877
17191
  cursor: pointer;
@@ -17763,7 +18077,317 @@ function upcomingPage(data) {
17763
18077
  `;
17764
18078
  }
17765
18079
 
18080
+ // src/web/templates/pages/sprint-summary.ts
18081
+ function progressBar(pct) {
18082
+ return `<div class="sprint-progress-bar">
18083
+ <div class="sprint-progress-fill" style="width: ${pct}%"></div>
18084
+ <span class="sprint-progress-label">${pct}%</span>
18085
+ </div>`;
18086
+ }
18087
+ function sprintSummaryPage(data, cached2) {
18088
+ if (!data) {
18089
+ return `
18090
+ <div class="page-header">
18091
+ <h2>Sprint Summary</h2>
18092
+ <div class="subtitle">AI-powered sprint narrative</div>
18093
+ </div>
18094
+ <div class="empty">
18095
+ <h3>No Active Sprint</h3>
18096
+ <p>No active sprint found. Create a sprint and set its status to "active" to see the summary.</p>
18097
+ </div>`;
18098
+ }
18099
+ const statsCards = `
18100
+ <div class="cards">
18101
+ <div class="card">
18102
+ <div class="card-label">Completion</div>
18103
+ <div class="card-value">${data.workItems.completionPct}%</div>
18104
+ <div class="card-sub">${data.workItems.done} / ${data.workItems.total} items done</div>
18105
+ </div>
18106
+ <div class="card">
18107
+ <div class="card-label">Days Remaining</div>
18108
+ <div class="card-value">${data.timeline.daysRemaining}</div>
18109
+ <div class="card-sub">${data.timeline.daysElapsed} of ${data.timeline.totalDays} elapsed</div>
18110
+ </div>
18111
+ <div class="card">
18112
+ <div class="card-label">Epics</div>
18113
+ <div class="card-value">${data.linkedEpics.length}</div>
18114
+ <div class="card-sub">linked to sprint</div>
18115
+ </div>
18116
+ <div class="card">
18117
+ <div class="card-label">Blockers</div>
18118
+ <div class="card-value${data.blockers.length > 0 ? " priority-high" : ""}">${data.blockers.length}</div>
18119
+ <div class="card-sub">${data.openActions.length} open actions</div>
18120
+ </div>
18121
+ </div>`;
18122
+ const epicsTable = data.linkedEpics.length > 0 ? collapsibleSection(
18123
+ "ss-epics",
18124
+ "Linked Epics",
18125
+ `<div class="table-wrap">
18126
+ <table>
18127
+ <thead>
18128
+ <tr><th>ID</th><th>Title</th><th>Status</th><th>Tasks</th></tr>
18129
+ </thead>
18130
+ <tbody>
18131
+ ${data.linkedEpics.map((e) => `
18132
+ <tr>
18133
+ <td><a href="/docs/epic/${escapeHtml(e.id)}">${escapeHtml(e.id)}</a></td>
18134
+ <td>${escapeHtml(e.title)}</td>
18135
+ <td>${statusBadge(e.status)}</td>
18136
+ <td>${e.tasksDone} / ${e.tasksTotal}</td>
18137
+ </tr>`).join("")}
18138
+ </tbody>
18139
+ </table>
18140
+ </div>`,
18141
+ { titleTag: "h3" }
18142
+ ) : "";
18143
+ const workItemsSection = data.workItems.total > 0 ? collapsibleSection(
18144
+ "ss-work-items",
18145
+ "Work Items",
18146
+ `<div class="table-wrap">
18147
+ <table>
18148
+ <thead>
18149
+ <tr><th>ID</th><th>Title</th><th>Type</th><th>Status</th></tr>
18150
+ </thead>
18151
+ <tbody>
18152
+ ${data.workItems.items.map((w) => `
18153
+ <tr>
18154
+ <td><a href="/docs/${escapeHtml(w.type)}/${escapeHtml(w.id)}">${escapeHtml(w.id)}</a></td>
18155
+ <td>${escapeHtml(w.title)}</td>
18156
+ <td>${escapeHtml(typeLabel(w.type))}</td>
18157
+ <td>${statusBadge(w.status)}</td>
18158
+ </tr>`).join("")}
18159
+ </tbody>
18160
+ </table>
18161
+ </div>`,
18162
+ { titleTag: "h3", defaultCollapsed: true }
18163
+ ) : "";
18164
+ const activitySection = data.artifacts.length > 0 ? collapsibleSection(
18165
+ "ss-activity",
18166
+ "Recent Activity",
18167
+ `<div class="table-wrap">
18168
+ <table>
18169
+ <thead>
18170
+ <tr><th>Date</th><th>ID</th><th>Title</th><th>Type</th><th>Action</th></tr>
18171
+ </thead>
18172
+ <tbody>
18173
+ ${data.artifacts.slice(0, 15).map((a) => `
18174
+ <tr>
18175
+ <td>${formatDate(a.date)}</td>
18176
+ <td><a href="/docs/${escapeHtml(a.type)}/${escapeHtml(a.id)}">${escapeHtml(a.id)}</a></td>
18177
+ <td>${escapeHtml(a.title)}</td>
18178
+ <td>${escapeHtml(typeLabel(a.type))}</td>
18179
+ <td>${escapeHtml(a.action)}</td>
18180
+ </tr>`).join("")}
18181
+ </tbody>
18182
+ </table>
18183
+ </div>`,
18184
+ { titleTag: "h3", defaultCollapsed: true }
18185
+ ) : "";
18186
+ const meetingsSection = data.meetings.length > 0 ? collapsibleSection(
18187
+ "ss-meetings",
18188
+ `Meetings (${data.meetings.length})`,
18189
+ `<div class="table-wrap">
18190
+ <table>
18191
+ <thead>
18192
+ <tr><th>Date</th><th>ID</th><th>Title</th></tr>
18193
+ </thead>
18194
+ <tbody>
18195
+ ${data.meetings.map((m) => `
18196
+ <tr>
18197
+ <td>${formatDate(m.date)}</td>
18198
+ <td><a href="/docs/meeting/${escapeHtml(m.id)}">${escapeHtml(m.id)}</a></td>
18199
+ <td>${escapeHtml(m.title)}</td>
18200
+ </tr>`).join("")}
18201
+ </tbody>
18202
+ </table>
18203
+ </div>`,
18204
+ { titleTag: "h3", defaultCollapsed: true }
18205
+ ) : "";
18206
+ const goalHtml = data.sprint.goal ? `<div class="sprint-goal"><strong>Goal:</strong> ${escapeHtml(data.sprint.goal)}</div>` : "";
18207
+ const dateRange = data.sprint.startDate && data.sprint.endDate ? `<span class="text-dim">${formatDate(data.sprint.startDate)} \u2014 ${formatDate(data.sprint.endDate)}</span>` : "";
18208
+ return `
18209
+ <div class="page-header">
18210
+ <h2>${escapeHtml(data.sprint.id)} \u2014 ${escapeHtml(data.sprint.title)} ${statusBadge(data.sprint.status)}</h2>
18211
+ <div class="subtitle">Sprint Summary ${dateRange}</div>
18212
+ </div>
18213
+ ${goalHtml}
18214
+ ${progressBar(data.timeline.percentComplete)}
18215
+ ${statsCards}
18216
+ ${epicsTable}
18217
+ ${workItemsSection}
18218
+ ${activitySection}
18219
+ ${meetingsSection}
18220
+
18221
+ <div class="sprint-ai-section">
18222
+ <h3>AI Summary</h3>
18223
+ ${cached2 ? `<p class="text-dim">Generated ${formatDate(cached2.generatedAt)} at ${cached2.generatedAt.slice(11, 16)} UTC</p>` : `<p class="text-dim">Generate a narrative summary of this sprint's progress, risks, and projections.</p>`}
18224
+ <button class="sprint-generate-btn" onclick="generateSummary()" id="generate-btn">${cached2 ? "Regenerate" : "Generate AI Summary"}</button>
18225
+ <div id="summary-loading" class="sprint-loading" style="display:none">
18226
+ <div class="sprint-spinner"></div>
18227
+ <span>Generating summary...</span>
18228
+ </div>
18229
+ <div id="summary-error" class="sprint-error" style="display:none"></div>
18230
+ <div id="summary-content" class="detail-content"${cached2 ? "" : ' style="display:none"'}>${cached2 ? cached2.html : ""}</div>
18231
+ </div>
18232
+
18233
+ <script>
18234
+ async function generateSummary() {
18235
+ var btn = document.getElementById('generate-btn');
18236
+ var loading = document.getElementById('summary-loading');
18237
+ var errorEl = document.getElementById('summary-error');
18238
+ var content = document.getElementById('summary-content');
18239
+
18240
+ btn.disabled = true;
18241
+ btn.style.display = 'none';
18242
+ loading.style.display = 'flex';
18243
+ errorEl.style.display = 'none';
18244
+ content.style.display = 'none';
18245
+
18246
+ try {
18247
+ var res = await fetch('/api/sprint-summary', {
18248
+ method: 'POST',
18249
+ headers: { 'Content-Type': 'application/json' },
18250
+ body: JSON.stringify({ sprintId: '${escapeHtml(data.sprint.id)}' })
18251
+ });
18252
+ var json = await res.json();
18253
+ if (!res.ok) throw new Error(json.error || 'Failed to generate summary');
18254
+ loading.style.display = 'none';
18255
+ content.innerHTML = json.html;
18256
+ content.style.display = 'block';
18257
+ btn.textContent = 'Regenerate';
18258
+ btn.style.display = '';
18259
+ btn.disabled = false;
18260
+ } catch (e) {
18261
+ loading.style.display = 'none';
18262
+ errorEl.textContent = e.message;
18263
+ errorEl.style.display = 'block';
18264
+ btn.style.display = '';
18265
+ btn.disabled = false;
18266
+ }
18267
+ }
18268
+ </script>`;
18269
+ }
18270
+
18271
+ // src/reports/sprint-summary/generator.ts
18272
+ import { query } from "@anthropic-ai/claude-agent-sdk";
18273
+ async function generateSprintSummary(data) {
18274
+ const prompt = buildPrompt(data);
18275
+ const result = query({
18276
+ prompt,
18277
+ options: {
18278
+ systemPrompt: SYSTEM_PROMPT,
18279
+ maxTurns: 1,
18280
+ tools: [],
18281
+ allowedTools: []
18282
+ }
18283
+ });
18284
+ for await (const msg of result) {
18285
+ if (msg.type === "assistant") {
18286
+ const text = msg.message.content.find(
18287
+ (b) => b.type === "text"
18288
+ );
18289
+ if (text) return text.text;
18290
+ }
18291
+ }
18292
+ return "Unable to generate sprint summary.";
18293
+ }
18294
+ var SYSTEM_PROMPT = `You are a delivery management assistant generating a sprint summary narrative. Produce a concise, insightful markdown report. Do NOT include a top-level heading \u2014 the caller will add one. Use the following structure:
18295
+
18296
+ ## Sprint Health
18297
+ One-line verdict on overall sprint health (healthy / at risk / behind).
18298
+
18299
+ ## Goal Progress
18300
+ How close the team is to achieving the sprint goal. Reference the goal text and completion metrics.
18301
+
18302
+ ## Key Achievements
18303
+ Notable completions, decisions made, meetings held during the sprint. Use bullet points.
18304
+
18305
+ ## Current Risks
18306
+ Blockers, overdue items, unresolved questions, items without owners. Use bullet points. If none, say so.
18307
+
18308
+ ## Outcome Projection
18309
+ Given the current pace and time remaining, what's the likely outcome? Will the sprint goal be met?
18310
+
18311
+ Be specific \u2014 reference artifact IDs, dates, and numbers from the data. Keep the tone professional but direct.`;
18312
+ function buildPrompt(data) {
18313
+ const sections = [];
18314
+ sections.push(`# Sprint: ${data.sprint.id} \u2014 ${data.sprint.title}`);
18315
+ sections.push(`Status: ${data.sprint.status}`);
18316
+ if (data.sprint.goal) sections.push(`Goal: ${data.sprint.goal}`);
18317
+ if (data.sprint.startDate) sections.push(`Start: ${data.sprint.startDate}`);
18318
+ if (data.sprint.endDate) sections.push(`End: ${data.sprint.endDate}`);
18319
+ sections.push(`
18320
+ ## Timeline`);
18321
+ sections.push(`Days elapsed: ${data.timeline.daysElapsed} / ${data.timeline.totalDays}`);
18322
+ sections.push(`Days remaining: ${data.timeline.daysRemaining}`);
18323
+ sections.push(`Timeline progress: ${data.timeline.percentComplete}%`);
18324
+ sections.push(`
18325
+ ## Work Items`);
18326
+ sections.push(`Total: ${data.workItems.total}, Done: ${data.workItems.done}, In Progress: ${data.workItems.inProgress}, Open: ${data.workItems.open}, Blocked: ${data.workItems.blocked}`);
18327
+ sections.push(`Completion: ${data.workItems.completionPct}%`);
18328
+ if (Object.keys(data.workItems.byType).length > 0) {
18329
+ sections.push(`By type: ${Object.entries(data.workItems.byType).map(([t, n]) => `${t}: ${n}`).join(", ")}`);
18330
+ }
18331
+ if (data.linkedEpics.length > 0) {
18332
+ sections.push(`
18333
+ ## Linked Epics`);
18334
+ for (const e of data.linkedEpics) {
18335
+ sections.push(`- ${e.id}: ${e.title} [${e.status}] \u2014 ${e.tasksDone}/${e.tasksTotal} tasks done`);
18336
+ }
18337
+ }
18338
+ if (data.meetings.length > 0) {
18339
+ sections.push(`
18340
+ ## Meetings During Sprint`);
18341
+ for (const m of data.meetings) {
18342
+ sections.push(`- ${m.date}: ${m.id} \u2014 ${m.title}`);
18343
+ }
18344
+ }
18345
+ if (data.artifacts.length > 0) {
18346
+ sections.push(`
18347
+ ## Artifacts Created/Updated During Sprint`);
18348
+ for (const a of data.artifacts.slice(0, 20)) {
18349
+ sections.push(`- ${a.date}: ${a.id} (${a.type}) ${a.action} \u2014 ${a.title}`);
18350
+ }
18351
+ if (data.artifacts.length > 20) {
18352
+ sections.push(`... and ${data.artifacts.length - 20} more`);
18353
+ }
18354
+ }
18355
+ if (data.openActions.length > 0) {
18356
+ sections.push(`
18357
+ ## Open Actions`);
18358
+ for (const a of data.openActions) {
18359
+ const owner = a.owner ?? "unowned";
18360
+ const due = a.dueDate ?? "no due date";
18361
+ sections.push(`- ${a.id}: ${a.title} (${owner}, ${due})`);
18362
+ }
18363
+ }
18364
+ if (data.openQuestions.length > 0) {
18365
+ sections.push(`
18366
+ ## Open Questions`);
18367
+ for (const q of data.openQuestions) {
18368
+ sections.push(`- ${q.id}: ${q.title}`);
18369
+ }
18370
+ }
18371
+ if (data.blockers.length > 0) {
18372
+ sections.push(`
18373
+ ## Blockers`);
18374
+ for (const b of data.blockers) {
18375
+ sections.push(`- ${b.id} (${b.type}): ${b.title}`);
18376
+ }
18377
+ }
18378
+ if (data.velocity) {
18379
+ sections.push(`
18380
+ ## Velocity`);
18381
+ sections.push(`Current sprint completion rate: ${data.velocity.currentCompletionRate}%`);
18382
+ if (data.velocity.previousSprintRate !== void 0) {
18383
+ sections.push(`Previous sprint (${data.velocity.previousSprintId}): ${data.velocity.previousSprintRate}%`);
18384
+ }
18385
+ }
18386
+ return sections.join("\n");
18387
+ }
18388
+
17766
18389
  // src/web/router.ts
18390
+ var sprintSummaryCache = /* @__PURE__ */ new Map();
17767
18391
  function handleRequest(req, res, store, projectName, navGroups) {
17768
18392
  const parsed = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
17769
18393
  const pathname = parsed.pathname;
@@ -17809,6 +18433,42 @@ function handleRequest(req, res, store, projectName, navGroups) {
17809
18433
  respond(res, layout({ title: "Upcoming", activePath: "/upcoming", projectName, navGroups }, body));
17810
18434
  return;
17811
18435
  }
18436
+ if (pathname === "/sprint-summary" && req.method === "GET") {
18437
+ const sprintId = parsed.searchParams.get("sprint") ?? void 0;
18438
+ const data = getSprintSummaryData(store, sprintId);
18439
+ const cached2 = data ? sprintSummaryCache.get(data.sprint.id) : void 0;
18440
+ const body = sprintSummaryPage(data, cached2 ? { html: cached2.html, generatedAt: cached2.generatedAt } : void 0);
18441
+ respond(res, layout({ title: "Sprint Summary", activePath: "/sprint-summary", projectName, navGroups }, body));
18442
+ return;
18443
+ }
18444
+ if (pathname === "/api/sprint-summary" && req.method === "POST") {
18445
+ let bodyStr = "";
18446
+ req.on("data", (chunk) => {
18447
+ bodyStr += chunk;
18448
+ });
18449
+ req.on("end", async () => {
18450
+ try {
18451
+ const { sprintId } = JSON.parse(bodyStr || "{}");
18452
+ const data = getSprintSummaryData(store, sprintId);
18453
+ if (!data) {
18454
+ res.writeHead(404, { "Content-Type": "application/json" });
18455
+ res.end(JSON.stringify({ error: "Sprint not found" }));
18456
+ return;
18457
+ }
18458
+ const summary = await generateSprintSummary(data);
18459
+ const html = renderMarkdown(summary);
18460
+ const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
18461
+ sprintSummaryCache.set(data.sprint.id, { html, generatedAt });
18462
+ res.writeHead(200, { "Content-Type": "application/json" });
18463
+ res.end(JSON.stringify({ summary, html, generatedAt }));
18464
+ } catch (err) {
18465
+ console.error("[marvin web] Sprint summary generation error:", err);
18466
+ res.writeHead(500, { "Content-Type": "application/json" });
18467
+ res.end(JSON.stringify({ error: "Failed to generate summary" }));
18468
+ }
18469
+ });
18470
+ return;
18471
+ }
17812
18472
  const boardMatch = pathname.match(/^\/board(?:\/([^/]+))?$/);
17813
18473
  if (boardMatch) {
17814
18474
  const type = boardMatch[1];
@@ -18338,6 +18998,25 @@ function createReportTools(store) {
18338
18998
  },
18339
18999
  { annotations: { readOnlyHint: true } }
18340
19000
  ),
19001
+ tool8(
19002
+ "generate_sprint_summary",
19003
+ "Generate an AI-powered narrative summary of a sprint's progress, health, achievements, risks, and projected outcome",
19004
+ {
19005
+ sprint: external_exports.string().optional().describe("Sprint ID (e.g. 'SP-001'). Omit for the active sprint.")
19006
+ },
19007
+ async (args) => {
19008
+ const data = collectSprintSummaryData(store, args.sprint);
19009
+ if (!data) {
19010
+ const msg = args.sprint ? `Sprint ${args.sprint} not found.` : "No active sprint found.";
19011
+ return { content: [{ type: "text", text: msg }], isError: true };
19012
+ }
19013
+ const summary = await generateSprintSummary(data);
19014
+ return {
19015
+ content: [{ type: "text", text: summary }]
19016
+ };
19017
+ },
19018
+ { annotations: { readOnlyHint: true } }
19019
+ ),
18341
19020
  tool8(
18342
19021
  "save_report",
18343
19022
  "Save a generated report as a persistent document",
@@ -21885,6 +22564,24 @@ function createWebTools(store, projectName, navGroups) {
21885
22564
  };
21886
22565
  },
21887
22566
  { annotations: { readOnlyHint: true } }
22567
+ ),
22568
+ tool22(
22569
+ "get_dashboard_sprint_summary",
22570
+ "Get sprint summary data for the active sprint or a specific sprint. Returns structured data about progress, epics, work items, meetings, and blockers. Works without the web server running.",
22571
+ {
22572
+ sprint: external_exports.string().optional().describe("Sprint ID (e.g. 'SP-001'). Omit for the active sprint.")
22573
+ },
22574
+ async (args) => {
22575
+ const data = getSprintSummaryData(store, args.sprint);
22576
+ if (!data) {
22577
+ const msg = args.sprint ? `Sprint ${args.sprint} not found.` : "No active sprint found.";
22578
+ return { content: [{ type: "text", text: msg }], isError: true };
22579
+ }
22580
+ return {
22581
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
22582
+ };
22583
+ },
22584
+ { annotations: { readOnlyHint: true } }
21888
22585
  )
21889
22586
  ];
21890
22587
  }
@@ -21916,7 +22613,7 @@ import * as readline from "readline";
21916
22613
  import chalk from "chalk";
21917
22614
  import ora from "ora";
21918
22615
  import {
21919
- query as query2
22616
+ query as query3
21920
22617
  } from "@anthropic-ai/claude-agent-sdk";
21921
22618
 
21922
22619
  // src/storage/session-store.ts
@@ -21987,11 +22684,11 @@ var SessionStore = class {
21987
22684
  };
21988
22685
 
21989
22686
  // src/agent/session-namer.ts
21990
- import { query } from "@anthropic-ai/claude-agent-sdk";
22687
+ import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
21991
22688
  async function generateSessionName(turns) {
21992
22689
  try {
21993
22690
  const transcript = turns.slice(-20).map((t) => `${t.role}: ${t.content.slice(0, 200)}`).join("\n");
21994
- const result = query({
22691
+ const result = query2({
21995
22692
  prompt: `Summarize this conversation in 3-5 words as a kebab-case name suitable for a filename. Output ONLY the name, nothing else.
21996
22693
 
21997
22694
  ${transcript}`,
@@ -22258,6 +22955,7 @@ Marvin \u2014 ${persona.name}
22258
22955
  "mcp__marvin-governance__get_dashboard_gar",
22259
22956
  "mcp__marvin-governance__get_dashboard_board",
22260
22957
  "mcp__marvin-governance__get_dashboard_upcoming",
22958
+ "mcp__marvin-governance__get_dashboard_sprint_summary",
22261
22959
  ...pluginTools.map((t) => `mcp__marvin-governance__${t.name}`),
22262
22960
  ...codeSkillTools.map((t) => `mcp__marvin-governance__${t.name}`)
22263
22961
  ]
@@ -22268,7 +22966,7 @@ Marvin \u2014 ${persona.name}
22268
22966
  if (existingSession) {
22269
22967
  queryOptions.resume = existingSession.id;
22270
22968
  }
22271
- const conversation = query2({
22969
+ const conversation = query3({
22272
22970
  prompt,
22273
22971
  options: queryOptions
22274
22972
  });
@@ -22360,7 +23058,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
22360
23058
  import { tool as tool23 } from "@anthropic-ai/claude-agent-sdk";
22361
23059
 
22362
23060
  // src/skills/action-runner.ts
22363
- import { query as query3 } from "@anthropic-ai/claude-agent-sdk";
23061
+ import { query as query4 } from "@anthropic-ai/claude-agent-sdk";
22364
23062
  var GOVERNANCE_TOOL_NAMES2 = [
22365
23063
  "mcp__marvin-governance__list_decisions",
22366
23064
  "mcp__marvin-governance__get_decision",
@@ -22382,7 +23080,7 @@ async function runSkillAction(action, userPrompt, context) {
22382
23080
  try {
22383
23081
  const mcpServer = createMarvinMcpServer(context.store);
22384
23082
  const allowedTools = action.allowGovernanceTools !== false ? GOVERNANCE_TOOL_NAMES2 : [];
22385
- const conversation = query3({
23083
+ const conversation = query4({
22386
23084
  prompt: userPrompt,
22387
23085
  options: {
22388
23086
  systemPrompt: action.systemPrompt,
@@ -23176,7 +23874,7 @@ import * as fs13 from "fs";
23176
23874
  import * as path13 from "path";
23177
23875
  import chalk7 from "chalk";
23178
23876
  import ora2 from "ora";
23179
- import { query as query4 } from "@anthropic-ai/claude-agent-sdk";
23877
+ import { query as query5 } from "@anthropic-ai/claude-agent-sdk";
23180
23878
 
23181
23879
  // src/sources/prompts.ts
23182
23880
  function buildIngestSystemPrompt(persona, projectConfig, isDraft) {
@@ -23309,7 +24007,7 @@ async function ingestFile(options) {
23309
24007
  const spinner = ora2({ text: `Analyzing ${fileName}...`, color: "cyan" });
23310
24008
  spinner.start();
23311
24009
  try {
23312
- const conversation = query4({
24010
+ const conversation = query5({
23313
24011
  prompt: userPrompt,
23314
24012
  options: {
23315
24013
  systemPrompt,
@@ -24530,7 +25228,7 @@ import chalk13 from "chalk";
24530
25228
  // src/analysis/analyze.ts
24531
25229
  import chalk12 from "chalk";
24532
25230
  import ora4 from "ora";
24533
- import { query as query5 } from "@anthropic-ai/claude-agent-sdk";
25231
+ import { query as query6 } from "@anthropic-ai/claude-agent-sdk";
24534
25232
 
24535
25233
  // src/analysis/prompts.ts
24536
25234
  function buildAnalyzeSystemPrompt(persona, projectConfig, isDraft) {
@@ -24660,7 +25358,7 @@ async function analyzeMeeting(options) {
24660
25358
  const spinner = ora4({ text: `Analyzing meeting ${meetingId}...`, color: "cyan" });
24661
25359
  spinner.start();
24662
25360
  try {
24663
- const conversation = query5({
25361
+ const conversation = query6({
24664
25362
  prompt: userPrompt,
24665
25363
  options: {
24666
25364
  systemPrompt,
@@ -24787,7 +25485,7 @@ import chalk15 from "chalk";
24787
25485
  // src/contributions/contribute.ts
24788
25486
  import chalk14 from "chalk";
24789
25487
  import ora5 from "ora";
24790
- import { query as query6 } from "@anthropic-ai/claude-agent-sdk";
25488
+ import { query as query7 } from "@anthropic-ai/claude-agent-sdk";
24791
25489
 
24792
25490
  // src/contributions/prompts.ts
24793
25491
  function buildContributeSystemPrompt(persona, contributionType, projectConfig, isDraft) {
@@ -25041,7 +25739,7 @@ async function contributeFromPersona(options) {
25041
25739
  "mcp__marvin-governance__get_action",
25042
25740
  "mcp__marvin-governance__get_question"
25043
25741
  ];
25044
- const conversation = query6({
25742
+ const conversation = query7({
25045
25743
  prompt: userPrompt,
25046
25744
  options: {
25047
25745
  systemPrompt,
@@ -25187,6 +25885,9 @@ Contribution: ${options.type}`));
25187
25885
  });
25188
25886
  }
25189
25887
 
25888
+ // src/cli/commands/report.ts
25889
+ import ora6 from "ora";
25890
+
25190
25891
  // src/reports/gar/render-ascii.ts
25191
25892
  import chalk16 from "chalk";
25192
25893
  var STATUS_DOT = {
@@ -25381,6 +26082,47 @@ async function healthReportCommand(options) {
25381
26082
  console.log(renderAscii2(report));
25382
26083
  }
25383
26084
  }
26085
+ async function sprintSummaryCommand(options) {
26086
+ const project = loadProject();
26087
+ const plugin = resolvePlugin(project.config.methodology);
26088
+ const pluginRegistrations = plugin?.documentTypeRegistrations ?? [];
26089
+ const allSkills = loadAllSkills(project.marvinDir);
26090
+ const allSkillIds = [...allSkills.keys()];
26091
+ const skillRegistrations = collectSkillRegistrations(allSkillIds, allSkills);
26092
+ const store = new DocumentStore(project.marvinDir, [...pluginRegistrations, ...skillRegistrations]);
26093
+ const data = collectSprintSummaryData(store, options.sprint);
26094
+ if (!data) {
26095
+ const msg = options.sprint ? `Sprint ${options.sprint} not found.` : "No active sprint found. Use --sprint <id> to specify one.";
26096
+ console.error(msg);
26097
+ process.exit(1);
26098
+ }
26099
+ const spinner = ora6({ text: "Generating AI sprint summary...", color: "cyan" }).start();
26100
+ try {
26101
+ const summary = await generateSprintSummary(data);
26102
+ spinner.stop();
26103
+ const header = `# Sprint Summary: ${data.sprint.id} \u2014 ${data.sprint.title}
26104
+
26105
+ `;
26106
+ console.log(header + summary);
26107
+ if (options.save) {
26108
+ const doc = store.create(
26109
+ "report",
26110
+ {
26111
+ title: `Sprint Summary: ${data.sprint.title}`,
26112
+ status: "final",
26113
+ tags: [`report-type:sprint-summary`, `sprint:${data.sprint.id}`]
26114
+ },
26115
+ summary
26116
+ );
26117
+ console.log(`
26118
+ Saved as ${doc.frontmatter.id}`);
26119
+ }
26120
+ } catch (err) {
26121
+ spinner.stop();
26122
+ console.error("Failed to generate sprint summary:", err);
26123
+ process.exit(1);
26124
+ }
26125
+ }
25384
26126
 
25385
26127
  // src/cli/commands/web.ts
25386
26128
  async function webCommand(options) {
@@ -25423,7 +26165,7 @@ function createProgram() {
25423
26165
  const program = new Command();
25424
26166
  program.name("marvin").description(
25425
26167
  "AI-powered product development assistant with Product Owner, Delivery Manager, and Technical Lead personas"
25426
- ).version("0.4.6");
26168
+ ).version("0.4.7");
25427
26169
  program.command("init").description("Initialize a new Marvin project in the current directory").action(async () => {
25428
26170
  await initCommand();
25429
26171
  });
@@ -25506,6 +26248,9 @@ function createProgram() {
25506
26248
  ).action(async (options) => {
25507
26249
  await healthReportCommand(options);
25508
26250
  });
26251
+ reportCmd.command("sprint-summary").description("Generate an AI-powered sprint summary narrative").option("--sprint <id>", "Sprint ID (defaults to active sprint)").option("--save", "Save the summary as a report document").action(async (options) => {
26252
+ await sprintSummaryCommand(options);
26253
+ });
25509
26254
  program.command("web").description("Launch a local web dashboard for project data").option("-p, --port <port>", "Port to listen on (default: 3000)").option("--no-open", "Don't auto-open the browser").action(async (options) => {
25510
26255
  await webCommand(options);
25511
26256
  });