mrvn-cli 0.4.6 → 0.4.8

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,249 @@ 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 primaryDocs = workItemDocs.filter((d) => d.frontmatter.type !== "contribution");
15476
+ const byStatus = {};
15477
+ const byType = {};
15478
+ let doneCount = 0;
15479
+ let inProgressCount = 0;
15480
+ let openCount = 0;
15481
+ let blockedCount = 0;
15482
+ for (const doc of primaryDocs) {
15483
+ const s = doc.frontmatter.status;
15484
+ byStatus[s] = (byStatus[s] ?? 0) + 1;
15485
+ byType[doc.frontmatter.type] = (byType[doc.frontmatter.type] ?? 0) + 1;
15486
+ if (DONE_STATUSES.has(s)) doneCount++;
15487
+ else if (s === "in-progress") inProgressCount++;
15488
+ else if (s === "blocked") blockedCount++;
15489
+ else openCount++;
15490
+ }
15491
+ const allItemsById = /* @__PURE__ */ new Map();
15492
+ const childrenByParent = /* @__PURE__ */ new Map();
15493
+ const sprintItemIds = new Set(workItemDocs.map((d) => d.frontmatter.id));
15494
+ for (const doc of workItemDocs) {
15495
+ const about = doc.frontmatter.aboutArtifact;
15496
+ const item = {
15497
+ id: doc.frontmatter.id,
15498
+ title: doc.frontmatter.title,
15499
+ type: doc.frontmatter.type,
15500
+ status: doc.frontmatter.status,
15501
+ aboutArtifact: about
15502
+ };
15503
+ allItemsById.set(item.id, item);
15504
+ if (about && sprintItemIds.has(about)) {
15505
+ if (!childrenByParent.has(about)) childrenByParent.set(about, []);
15506
+ childrenByParent.get(about).push(item);
15507
+ }
15508
+ }
15509
+ const itemsWithChildren = /* @__PURE__ */ new Set();
15510
+ for (const [parentId, children] of childrenByParent) {
15511
+ const parent = allItemsById.get(parentId);
15512
+ if (parent) {
15513
+ parent.children = children;
15514
+ for (const child of children) itemsWithChildren.add(child.id);
15515
+ }
15516
+ }
15517
+ for (const item of allItemsById.values()) {
15518
+ if (item.children) {
15519
+ for (const child of item.children) {
15520
+ const grandchildren = childrenByParent.get(child.id);
15521
+ if (grandchildren) {
15522
+ child.children = grandchildren;
15523
+ for (const gc of grandchildren) itemsWithChildren.add(gc.id);
15524
+ }
15525
+ }
15526
+ }
15527
+ }
15528
+ const items = [];
15529
+ for (const doc of workItemDocs) {
15530
+ if (!itemsWithChildren.has(doc.frontmatter.id)) {
15531
+ items.push(allItemsById.get(doc.frontmatter.id));
15532
+ }
15533
+ }
15534
+ const workItems = {
15535
+ total: primaryDocs.length,
15536
+ done: doneCount,
15537
+ inProgress: inProgressCount,
15538
+ open: openCount,
15539
+ blocked: blockedCount,
15540
+ completionPct: primaryDocs.length > 0 ? Math.round(doneCount / primaryDocs.length * 100) : 0,
15541
+ byStatus,
15542
+ byType,
15543
+ items
15544
+ };
15545
+ const meetings = [];
15546
+ if (startDate && endDate) {
15547
+ const meetingDocs = allDocs.filter((d) => d.frontmatter.type === "meeting");
15548
+ for (const m of meetingDocs) {
15549
+ const meetingDate = m.frontmatter.date ?? m.frontmatter.created.slice(0, 10);
15550
+ if (meetingDate >= startDate && meetingDate <= endDate) {
15551
+ meetings.push({
15552
+ id: m.frontmatter.id,
15553
+ title: m.frontmatter.title,
15554
+ date: meetingDate
15555
+ });
15556
+ }
15557
+ }
15558
+ meetings.sort((a, b) => a.date.localeCompare(b.date));
15559
+ }
15560
+ const artifacts = [];
15561
+ if (startDate && endDate) {
15562
+ for (const doc of allDocs) {
15563
+ if (doc.frontmatter.type === "sprint") continue;
15564
+ const created = doc.frontmatter.created.slice(0, 10);
15565
+ const updated = doc.frontmatter.updated.slice(0, 10);
15566
+ if (created >= startDate && created <= endDate) {
15567
+ artifacts.push({
15568
+ id: doc.frontmatter.id,
15569
+ title: doc.frontmatter.title,
15570
+ type: doc.frontmatter.type,
15571
+ action: "created",
15572
+ date: created
15573
+ });
15574
+ } else if (updated >= startDate && updated <= endDate && updated !== created) {
15575
+ artifacts.push({
15576
+ id: doc.frontmatter.id,
15577
+ title: doc.frontmatter.title,
15578
+ type: doc.frontmatter.type,
15579
+ action: "updated",
15580
+ date: updated
15581
+ });
15582
+ }
15583
+ }
15584
+ artifacts.sort((a, b) => b.date.localeCompare(a.date));
15585
+ }
15586
+ const relevantTags = /* @__PURE__ */ new Set([sprintTag, ...linkedEpicIds.map((id) => `epic:${id}`)]);
15587
+ const openActions = allDocs.filter(
15588
+ (d) => d.frontmatter.type === "action" && !DONE_STATUSES.has(d.frontmatter.status) && d.frontmatter.tags?.some((t) => relevantTags.has(t))
15589
+ ).map((d) => ({
15590
+ id: d.frontmatter.id,
15591
+ title: d.frontmatter.title,
15592
+ owner: d.frontmatter.owner,
15593
+ dueDate: d.frontmatter.dueDate
15594
+ }));
15595
+ const openQuestions = allDocs.filter(
15596
+ (d) => d.frontmatter.type === "question" && d.frontmatter.status === "open" && d.frontmatter.tags?.some((t) => relevantTags.has(t))
15597
+ ).map((d) => ({
15598
+ id: d.frontmatter.id,
15599
+ title: d.frontmatter.title
15600
+ }));
15601
+ const blockers = allDocs.filter(
15602
+ (d) => d.frontmatter.status === "blocked" && d.frontmatter.tags?.includes(sprintTag)
15603
+ ).map((d) => ({
15604
+ id: d.frontmatter.id,
15605
+ title: d.frontmatter.title,
15606
+ type: d.frontmatter.type
15607
+ }));
15608
+ const riskBlockers = allDocs.filter(
15609
+ (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)
15610
+ );
15611
+ for (const d of riskBlockers) {
15612
+ blockers.push({
15613
+ id: d.frontmatter.id,
15614
+ title: d.frontmatter.title,
15615
+ type: d.frontmatter.type
15616
+ });
15617
+ }
15618
+ let velocity = null;
15619
+ const currentRate = workItems.completionPct;
15620
+ 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 ?? ""));
15621
+ if (completedSprints.length > 0) {
15622
+ const prev = completedSprints[0];
15623
+ const prevTag = `sprint:${prev.frontmatter.id}`;
15624
+ const prevWorkItems = allDocs.filter(
15625
+ (d) => d.frontmatter.type !== "sprint" && d.frontmatter.type !== "epic" && d.frontmatter.type !== "contribution" && d.frontmatter.tags?.includes(prevTag)
15626
+ );
15627
+ const prevDone = prevWorkItems.filter((d) => DONE_STATUSES.has(d.frontmatter.status)).length;
15628
+ const prevRate = prevWorkItems.length > 0 ? Math.round(prevDone / prevWorkItems.length * 100) : 0;
15629
+ velocity = {
15630
+ currentCompletionRate: currentRate,
15631
+ previousSprintRate: prevRate,
15632
+ previousSprintId: prev.frontmatter.id
15633
+ };
15634
+ } else {
15635
+ velocity = { currentCompletionRate: currentRate };
15636
+ }
15637
+ return {
15638
+ sprint: {
15639
+ id: fm.id,
15640
+ title: fm.title,
15641
+ goal: fm.goal,
15642
+ status: fm.status,
15643
+ startDate,
15644
+ endDate
15645
+ },
15646
+ timeline: { daysElapsed, daysRemaining, totalDays, percentComplete },
15647
+ linkedEpics,
15648
+ workItems,
15649
+ meetings,
15650
+ artifacts,
15651
+ openActions,
15652
+ openQuestions,
15653
+ blockers,
15654
+ velocity
15655
+ };
15656
+ }
15657
+
15415
15658
  // src/web/data.ts
15416
15659
  function getOverviewData(store) {
15417
15660
  const types = [];
@@ -15533,7 +15776,7 @@ function computeUrgency(dueDateStr, todayStr) {
15533
15776
  if (diffDays <= 14) return "upcoming";
15534
15777
  return "later";
15535
15778
  }
15536
- var DONE_STATUSES = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
15779
+ var DONE_STATUSES2 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
15537
15780
  function getUpcomingData(store) {
15538
15781
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
15539
15782
  const allDocs = store.list();
@@ -15542,7 +15785,7 @@ function getUpcomingData(store) {
15542
15785
  docById.set(doc.frontmatter.id, doc);
15543
15786
  }
15544
15787
  const actions = allDocs.filter(
15545
- (d) => d.frontmatter.type === "action" && !DONE_STATUSES.has(d.frontmatter.status)
15788
+ (d) => d.frontmatter.type === "action" && !DONE_STATUSES2.has(d.frontmatter.status)
15546
15789
  );
15547
15790
  const actionsWithDue = actions.filter((d) => d.frontmatter.dueDate);
15548
15791
  const sprints = allDocs.filter((d) => d.frontmatter.type === "sprint");
@@ -15606,7 +15849,7 @@ function getUpcomingData(store) {
15606
15849
  const sprintEnd = sprint.frontmatter.endDate;
15607
15850
  const sprintTaskDocs = getSprintTasks(sprint);
15608
15851
  for (const task of sprintTaskDocs) {
15609
- if (DONE_STATUSES.has(task.frontmatter.status)) continue;
15852
+ if (DONE_STATUSES2.has(task.frontmatter.status)) continue;
15610
15853
  const existing = taskSprintMap.get(task.frontmatter.id);
15611
15854
  if (!existing || sprintEnd < existing.sprintEnd) {
15612
15855
  taskSprintMap.set(task.frontmatter.id, { task, sprint, sprintEnd });
@@ -15623,7 +15866,7 @@ function getUpcomingData(store) {
15623
15866
  urgency: computeUrgency(sprintEnd, today)
15624
15867
  })).sort((a, b) => a.sprintEndDate.localeCompare(b.sprintEndDate));
15625
15868
  const openItems = allDocs.filter(
15626
- (d) => ["action", "question", "task"].includes(d.frontmatter.type) && !DONE_STATUSES.has(d.frontmatter.status)
15869
+ (d) => ["action", "question", "task"].includes(d.frontmatter.type) && !DONE_STATUSES2.has(d.frontmatter.status)
15627
15870
  );
15628
15871
  const fourteenDaysAgo = new Date(todayMs - fourteenDaysMs).toISOString().slice(0, 10);
15629
15872
  const recentMeetings = allDocs.filter(
@@ -15721,6 +15964,9 @@ function getUpcomingData(store) {
15721
15964
  }).filter((item) => item.score > 0).sort((a, b) => b.score - a.score).slice(0, 15);
15722
15965
  return { dueSoonActions, dueSoonSprintTasks, trending };
15723
15966
  }
15967
+ function getSprintSummaryData(store, sprintId) {
15968
+ return collectSprintSummaryData(store, sprintId);
15969
+ }
15724
15970
 
15725
15971
  // src/web/templates/layout.ts
15726
15972
  function collapsibleSection(sectionId, title, content, opts) {
@@ -15862,6 +16108,7 @@ function layout(opts, body) {
15862
16108
  const topItems = [
15863
16109
  { href: "/", label: "Overview" },
15864
16110
  { href: "/upcoming", label: "Upcoming" },
16111
+ { href: "/sprint-summary", label: "Sprint Summary" },
15865
16112
  { href: "/timeline", label: "Timeline" },
15866
16113
  { href: "/board", label: "Board" },
15867
16114
  { href: "/gar", label: "GAR Report" },
@@ -16245,6 +16492,17 @@ tr:hover td {
16245
16492
  background: var(--bg-hover);
16246
16493
  }
16247
16494
 
16495
+ /* Hierarchical work-item sub-rows */
16496
+ .child-row td {
16497
+ font-size: 0.8125rem;
16498
+ border-bottom-style: dashed;
16499
+ }
16500
+ .contribution-row td {
16501
+ font-size: 0.8125rem;
16502
+ color: var(--text-dim);
16503
+ border-bottom-style: dashed;
16504
+ }
16505
+
16248
16506
  /* GAR */
16249
16507
  .gar-overall {
16250
16508
  text-align: center;
@@ -16872,6 +17130,112 @@ tr:hover td {
16872
17130
 
16873
17131
  .text-dim { color: var(--text-dim); }
16874
17132
 
17133
+ /* Sprint Summary */
17134
+ .sprint-goal {
17135
+ background: var(--bg-card);
17136
+ border: 1px solid var(--border);
17137
+ border-radius: var(--radius);
17138
+ padding: 0.75rem 1rem;
17139
+ margin-bottom: 1rem;
17140
+ font-size: 0.9rem;
17141
+ color: var(--text);
17142
+ }
17143
+
17144
+ .sprint-progress-bar {
17145
+ position: relative;
17146
+ height: 24px;
17147
+ background: var(--bg-card);
17148
+ border: 1px solid var(--border);
17149
+ border-radius: 12px;
17150
+ margin-bottom: 1.25rem;
17151
+ overflow: hidden;
17152
+ }
17153
+
17154
+ .sprint-progress-fill {
17155
+ height: 100%;
17156
+ background: linear-gradient(90deg, var(--accent-dim), var(--accent));
17157
+ border-radius: 12px;
17158
+ transition: width 0.3s ease;
17159
+ }
17160
+
17161
+ .sprint-progress-label {
17162
+ position: absolute;
17163
+ top: 50%;
17164
+ left: 50%;
17165
+ transform: translate(-50%, -50%);
17166
+ font-size: 0.7rem;
17167
+ font-weight: 700;
17168
+ color: var(--text);
17169
+ }
17170
+
17171
+ .sprint-ai-section {
17172
+ margin-top: 2rem;
17173
+ background: var(--bg-card);
17174
+ border: 1px solid var(--border);
17175
+ border-radius: var(--radius);
17176
+ padding: 1.5rem;
17177
+ }
17178
+
17179
+ .sprint-ai-section h3 {
17180
+ font-size: 1rem;
17181
+ font-weight: 600;
17182
+ margin-bottom: 0.5rem;
17183
+ }
17184
+
17185
+ .sprint-generate-btn {
17186
+ background: var(--accent);
17187
+ color: #fff;
17188
+ border: none;
17189
+ border-radius: var(--radius);
17190
+ padding: 0.5rem 1.25rem;
17191
+ font-size: 0.85rem;
17192
+ font-weight: 600;
17193
+ cursor: pointer;
17194
+ margin-top: 0.75rem;
17195
+ transition: background 0.15s;
17196
+ }
17197
+
17198
+ .sprint-generate-btn:hover:not(:disabled) {
17199
+ background: var(--accent-dim);
17200
+ }
17201
+
17202
+ .sprint-generate-btn:disabled {
17203
+ opacity: 0.5;
17204
+ cursor: not-allowed;
17205
+ }
17206
+
17207
+ .sprint-loading {
17208
+ display: flex;
17209
+ align-items: center;
17210
+ gap: 0.75rem;
17211
+ padding: 1rem 0;
17212
+ color: var(--text-dim);
17213
+ font-size: 0.85rem;
17214
+ }
17215
+
17216
+ .sprint-spinner {
17217
+ width: 20px;
17218
+ height: 20px;
17219
+ border: 2px solid var(--border);
17220
+ border-top-color: var(--accent);
17221
+ border-radius: 50%;
17222
+ animation: sprint-spin 0.8s linear infinite;
17223
+ }
17224
+
17225
+ @keyframes sprint-spin {
17226
+ to { transform: rotate(360deg); }
17227
+ }
17228
+
17229
+ .sprint-error {
17230
+ color: var(--red);
17231
+ font-size: 0.85rem;
17232
+ padding: 0.5rem 0;
17233
+ }
17234
+
17235
+ .sprint-ai-section .detail-content {
17236
+ margin-top: 1rem;
17237
+ }
17238
+
16875
17239
  /* Collapsible sections */
16876
17240
  .collapsible-header {
16877
17241
  cursor: pointer;
@@ -17763,7 +18127,329 @@ function upcomingPage(data) {
17763
18127
  `;
17764
18128
  }
17765
18129
 
18130
+ // src/web/templates/pages/sprint-summary.ts
18131
+ function progressBar(pct) {
18132
+ return `<div class="sprint-progress-bar">
18133
+ <div class="sprint-progress-fill" style="width: ${pct}%"></div>
18134
+ <span class="sprint-progress-label">${pct}%</span>
18135
+ </div>`;
18136
+ }
18137
+ function sprintSummaryPage(data, cached2) {
18138
+ if (!data) {
18139
+ return `
18140
+ <div class="page-header">
18141
+ <h2>Sprint Summary</h2>
18142
+ <div class="subtitle">AI-powered sprint narrative</div>
18143
+ </div>
18144
+ <div class="empty">
18145
+ <h3>No Active Sprint</h3>
18146
+ <p>No active sprint found. Create a sprint and set its status to "active" to see the summary.</p>
18147
+ </div>`;
18148
+ }
18149
+ const statsCards = `
18150
+ <div class="cards">
18151
+ <div class="card">
18152
+ <div class="card-label">Completion</div>
18153
+ <div class="card-value">${data.workItems.completionPct}%</div>
18154
+ <div class="card-sub">${data.workItems.done} / ${data.workItems.total} items done</div>
18155
+ </div>
18156
+ <div class="card">
18157
+ <div class="card-label">Days Remaining</div>
18158
+ <div class="card-value">${data.timeline.daysRemaining}</div>
18159
+ <div class="card-sub">${data.timeline.daysElapsed} of ${data.timeline.totalDays} elapsed</div>
18160
+ </div>
18161
+ <div class="card">
18162
+ <div class="card-label">Epics</div>
18163
+ <div class="card-value">${data.linkedEpics.length}</div>
18164
+ <div class="card-sub">linked to sprint</div>
18165
+ </div>
18166
+ <div class="card">
18167
+ <div class="card-label">Blockers</div>
18168
+ <div class="card-value${data.blockers.length > 0 ? " priority-high" : ""}">${data.blockers.length}</div>
18169
+ <div class="card-sub">${data.openActions.length} open actions</div>
18170
+ </div>
18171
+ </div>`;
18172
+ const epicsTable = data.linkedEpics.length > 0 ? collapsibleSection(
18173
+ "ss-epics",
18174
+ "Linked Epics",
18175
+ `<div class="table-wrap">
18176
+ <table>
18177
+ <thead>
18178
+ <tr><th>ID</th><th>Title</th><th>Status</th><th>Tasks</th></tr>
18179
+ </thead>
18180
+ <tbody>
18181
+ ${data.linkedEpics.map((e) => `
18182
+ <tr>
18183
+ <td><a href="/docs/epic/${escapeHtml(e.id)}">${escapeHtml(e.id)}</a></td>
18184
+ <td>${escapeHtml(e.title)}</td>
18185
+ <td>${statusBadge(e.status)}</td>
18186
+ <td>${e.tasksDone} / ${e.tasksTotal}</td>
18187
+ </tr>`).join("")}
18188
+ </tbody>
18189
+ </table>
18190
+ </div>`,
18191
+ { titleTag: "h3" }
18192
+ ) : "";
18193
+ function renderItemRows(items, depth = 0) {
18194
+ return items.flatMap((w) => {
18195
+ const isChild = depth > 0;
18196
+ const isContribution = w.type === "contribution";
18197
+ const rowClass = isContribution ? ' class="contribution-row"' : isChild ? ' class="child-row"' : "";
18198
+ const indent = depth > 0 ? ` style="padding-left: ${0.75 + depth * 1}rem"` : "";
18199
+ const row = `
18200
+ <tr${rowClass}>
18201
+ <td${indent}><a href="/docs/${escapeHtml(w.type)}/${escapeHtml(w.id)}">${escapeHtml(w.id)}</a></td>
18202
+ <td>${escapeHtml(w.title)}</td>
18203
+ <td>${escapeHtml(typeLabel(w.type))}</td>
18204
+ <td>${statusBadge(w.status)}</td>
18205
+ </tr>`;
18206
+ const childRows = w.children ? renderItemRows(w.children, depth + 1) : [];
18207
+ return [row, ...childRows];
18208
+ });
18209
+ }
18210
+ const workItemRows = renderItemRows(data.workItems.items);
18211
+ const workItemsSection = workItemRows.length > 0 ? collapsibleSection(
18212
+ "ss-work-items",
18213
+ "Work Items",
18214
+ `<div class="table-wrap">
18215
+ <table>
18216
+ <thead>
18217
+ <tr><th>ID</th><th>Title</th><th>Type</th><th>Status</th></tr>
18218
+ </thead>
18219
+ <tbody>
18220
+ ${workItemRows.join("")}
18221
+ </tbody>
18222
+ </table>
18223
+ </div>`,
18224
+ { titleTag: "h3", defaultCollapsed: true }
18225
+ ) : "";
18226
+ const activitySection = data.artifacts.length > 0 ? collapsibleSection(
18227
+ "ss-activity",
18228
+ "Recent Activity",
18229
+ `<div class="table-wrap">
18230
+ <table>
18231
+ <thead>
18232
+ <tr><th>Date</th><th>ID</th><th>Title</th><th>Type</th><th>Action</th></tr>
18233
+ </thead>
18234
+ <tbody>
18235
+ ${data.artifacts.slice(0, 15).map((a) => `
18236
+ <tr>
18237
+ <td>${formatDate(a.date)}</td>
18238
+ <td><a href="/docs/${escapeHtml(a.type)}/${escapeHtml(a.id)}">${escapeHtml(a.id)}</a></td>
18239
+ <td>${escapeHtml(a.title)}</td>
18240
+ <td>${escapeHtml(typeLabel(a.type))}</td>
18241
+ <td>${escapeHtml(a.action)}</td>
18242
+ </tr>`).join("")}
18243
+ </tbody>
18244
+ </table>
18245
+ </div>`,
18246
+ { titleTag: "h3", defaultCollapsed: true }
18247
+ ) : "";
18248
+ const meetingsSection = data.meetings.length > 0 ? collapsibleSection(
18249
+ "ss-meetings",
18250
+ `Meetings (${data.meetings.length})`,
18251
+ `<div class="table-wrap">
18252
+ <table>
18253
+ <thead>
18254
+ <tr><th>Date</th><th>ID</th><th>Title</th></tr>
18255
+ </thead>
18256
+ <tbody>
18257
+ ${data.meetings.map((m) => `
18258
+ <tr>
18259
+ <td>${formatDate(m.date)}</td>
18260
+ <td><a href="/docs/meeting/${escapeHtml(m.id)}">${escapeHtml(m.id)}</a></td>
18261
+ <td>${escapeHtml(m.title)}</td>
18262
+ </tr>`).join("")}
18263
+ </tbody>
18264
+ </table>
18265
+ </div>`,
18266
+ { titleTag: "h3", defaultCollapsed: true }
18267
+ ) : "";
18268
+ const goalHtml = data.sprint.goal ? `<div class="sprint-goal"><strong>Goal:</strong> ${escapeHtml(data.sprint.goal)}</div>` : "";
18269
+ const dateRange = data.sprint.startDate && data.sprint.endDate ? `<span class="text-dim">${formatDate(data.sprint.startDate)} \u2014 ${formatDate(data.sprint.endDate)}</span>` : "";
18270
+ return `
18271
+ <div class="page-header">
18272
+ <h2>${escapeHtml(data.sprint.id)} \u2014 ${escapeHtml(data.sprint.title)} ${statusBadge(data.sprint.status)}</h2>
18273
+ <div class="subtitle">Sprint Summary ${dateRange}</div>
18274
+ </div>
18275
+ ${goalHtml}
18276
+ ${progressBar(data.timeline.percentComplete)}
18277
+ ${statsCards}
18278
+ ${epicsTable}
18279
+ ${workItemsSection}
18280
+ ${activitySection}
18281
+ ${meetingsSection}
18282
+
18283
+ <div class="sprint-ai-section">
18284
+ <h3>AI Summary</h3>
18285
+ ${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>`}
18286
+ <button class="sprint-generate-btn" onclick="generateSummary()" id="generate-btn">${cached2 ? "Regenerate" : "Generate AI Summary"}</button>
18287
+ <div id="summary-loading" class="sprint-loading" style="display:none">
18288
+ <div class="sprint-spinner"></div>
18289
+ <span>Generating summary...</span>
18290
+ </div>
18291
+ <div id="summary-error" class="sprint-error" style="display:none"></div>
18292
+ <div id="summary-content" class="detail-content"${cached2 ? "" : ' style="display:none"'}>${cached2 ? cached2.html : ""}</div>
18293
+ </div>
18294
+
18295
+ <script>
18296
+ async function generateSummary() {
18297
+ var btn = document.getElementById('generate-btn');
18298
+ var loading = document.getElementById('summary-loading');
18299
+ var errorEl = document.getElementById('summary-error');
18300
+ var content = document.getElementById('summary-content');
18301
+
18302
+ btn.disabled = true;
18303
+ btn.style.display = 'none';
18304
+ loading.style.display = 'flex';
18305
+ errorEl.style.display = 'none';
18306
+ content.style.display = 'none';
18307
+
18308
+ try {
18309
+ var res = await fetch('/api/sprint-summary', {
18310
+ method: 'POST',
18311
+ headers: { 'Content-Type': 'application/json' },
18312
+ body: JSON.stringify({ sprintId: '${escapeHtml(data.sprint.id)}' })
18313
+ });
18314
+ var json = await res.json();
18315
+ if (!res.ok) throw new Error(json.error || 'Failed to generate summary');
18316
+ loading.style.display = 'none';
18317
+ content.innerHTML = json.html;
18318
+ content.style.display = 'block';
18319
+ btn.textContent = 'Regenerate';
18320
+ btn.style.display = '';
18321
+ btn.disabled = false;
18322
+ } catch (e) {
18323
+ loading.style.display = 'none';
18324
+ errorEl.textContent = e.message;
18325
+ errorEl.style.display = 'block';
18326
+ btn.style.display = '';
18327
+ btn.disabled = false;
18328
+ }
18329
+ }
18330
+ </script>`;
18331
+ }
18332
+
18333
+ // src/reports/sprint-summary/generator.ts
18334
+ import { query } from "@anthropic-ai/claude-agent-sdk";
18335
+ async function generateSprintSummary(data) {
18336
+ const prompt = buildPrompt(data);
18337
+ const result = query({
18338
+ prompt,
18339
+ options: {
18340
+ systemPrompt: SYSTEM_PROMPT,
18341
+ maxTurns: 1,
18342
+ tools: [],
18343
+ allowedTools: []
18344
+ }
18345
+ });
18346
+ for await (const msg of result) {
18347
+ if (msg.type === "assistant") {
18348
+ const text = msg.message.content.find(
18349
+ (b) => b.type === "text"
18350
+ );
18351
+ if (text) return text.text;
18352
+ }
18353
+ }
18354
+ return "Unable to generate sprint summary.";
18355
+ }
18356
+ 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:
18357
+
18358
+ ## Sprint Health
18359
+ One-line verdict on overall sprint health (healthy / at risk / behind).
18360
+
18361
+ ## Goal Progress
18362
+ How close the team is to achieving the sprint goal. Reference the goal text and completion metrics.
18363
+
18364
+ ## Key Achievements
18365
+ Notable completions, decisions made, meetings held during the sprint. Use bullet points.
18366
+
18367
+ ## Current Risks
18368
+ Blockers, overdue items, unresolved questions, items without owners. Use bullet points. If none, say so.
18369
+
18370
+ ## Outcome Projection
18371
+ Given the current pace and time remaining, what's the likely outcome? Will the sprint goal be met?
18372
+
18373
+ Be specific \u2014 reference artifact IDs, dates, and numbers from the data. Keep the tone professional but direct.`;
18374
+ function buildPrompt(data) {
18375
+ const sections = [];
18376
+ sections.push(`# Sprint: ${data.sprint.id} \u2014 ${data.sprint.title}`);
18377
+ sections.push(`Status: ${data.sprint.status}`);
18378
+ if (data.sprint.goal) sections.push(`Goal: ${data.sprint.goal}`);
18379
+ if (data.sprint.startDate) sections.push(`Start: ${data.sprint.startDate}`);
18380
+ if (data.sprint.endDate) sections.push(`End: ${data.sprint.endDate}`);
18381
+ sections.push(`
18382
+ ## Timeline`);
18383
+ sections.push(`Days elapsed: ${data.timeline.daysElapsed} / ${data.timeline.totalDays}`);
18384
+ sections.push(`Days remaining: ${data.timeline.daysRemaining}`);
18385
+ sections.push(`Timeline progress: ${data.timeline.percentComplete}%`);
18386
+ sections.push(`
18387
+ ## Work Items`);
18388
+ sections.push(`Total: ${data.workItems.total}, Done: ${data.workItems.done}, In Progress: ${data.workItems.inProgress}, Open: ${data.workItems.open}, Blocked: ${data.workItems.blocked}`);
18389
+ sections.push(`Completion: ${data.workItems.completionPct}%`);
18390
+ if (Object.keys(data.workItems.byType).length > 0) {
18391
+ sections.push(`By type: ${Object.entries(data.workItems.byType).map(([t, n]) => `${t}: ${n}`).join(", ")}`);
18392
+ }
18393
+ if (data.linkedEpics.length > 0) {
18394
+ sections.push(`
18395
+ ## Linked Epics`);
18396
+ for (const e of data.linkedEpics) {
18397
+ sections.push(`- ${e.id}: ${e.title} [${e.status}] \u2014 ${e.tasksDone}/${e.tasksTotal} tasks done`);
18398
+ }
18399
+ }
18400
+ if (data.meetings.length > 0) {
18401
+ sections.push(`
18402
+ ## Meetings During Sprint`);
18403
+ for (const m of data.meetings) {
18404
+ sections.push(`- ${m.date}: ${m.id} \u2014 ${m.title}`);
18405
+ }
18406
+ }
18407
+ if (data.artifacts.length > 0) {
18408
+ sections.push(`
18409
+ ## Artifacts Created/Updated During Sprint`);
18410
+ for (const a of data.artifacts.slice(0, 20)) {
18411
+ sections.push(`- ${a.date}: ${a.id} (${a.type}) ${a.action} \u2014 ${a.title}`);
18412
+ }
18413
+ if (data.artifacts.length > 20) {
18414
+ sections.push(`... and ${data.artifacts.length - 20} more`);
18415
+ }
18416
+ }
18417
+ if (data.openActions.length > 0) {
18418
+ sections.push(`
18419
+ ## Open Actions`);
18420
+ for (const a of data.openActions) {
18421
+ const owner = a.owner ?? "unowned";
18422
+ const due = a.dueDate ?? "no due date";
18423
+ sections.push(`- ${a.id}: ${a.title} (${owner}, ${due})`);
18424
+ }
18425
+ }
18426
+ if (data.openQuestions.length > 0) {
18427
+ sections.push(`
18428
+ ## Open Questions`);
18429
+ for (const q of data.openQuestions) {
18430
+ sections.push(`- ${q.id}: ${q.title}`);
18431
+ }
18432
+ }
18433
+ if (data.blockers.length > 0) {
18434
+ sections.push(`
18435
+ ## Blockers`);
18436
+ for (const b of data.blockers) {
18437
+ sections.push(`- ${b.id} (${b.type}): ${b.title}`);
18438
+ }
18439
+ }
18440
+ if (data.velocity) {
18441
+ sections.push(`
18442
+ ## Velocity`);
18443
+ sections.push(`Current sprint completion rate: ${data.velocity.currentCompletionRate}%`);
18444
+ if (data.velocity.previousSprintRate !== void 0) {
18445
+ sections.push(`Previous sprint (${data.velocity.previousSprintId}): ${data.velocity.previousSprintRate}%`);
18446
+ }
18447
+ }
18448
+ return sections.join("\n");
18449
+ }
18450
+
17766
18451
  // src/web/router.ts
18452
+ var sprintSummaryCache = /* @__PURE__ */ new Map();
17767
18453
  function handleRequest(req, res, store, projectName, navGroups) {
17768
18454
  const parsed = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
17769
18455
  const pathname = parsed.pathname;
@@ -17809,6 +18495,42 @@ function handleRequest(req, res, store, projectName, navGroups) {
17809
18495
  respond(res, layout({ title: "Upcoming", activePath: "/upcoming", projectName, navGroups }, body));
17810
18496
  return;
17811
18497
  }
18498
+ if (pathname === "/sprint-summary" && req.method === "GET") {
18499
+ const sprintId = parsed.searchParams.get("sprint") ?? void 0;
18500
+ const data = getSprintSummaryData(store, sprintId);
18501
+ const cached2 = data ? sprintSummaryCache.get(data.sprint.id) : void 0;
18502
+ const body = sprintSummaryPage(data, cached2 ? { html: cached2.html, generatedAt: cached2.generatedAt } : void 0);
18503
+ respond(res, layout({ title: "Sprint Summary", activePath: "/sprint-summary", projectName, navGroups }, body));
18504
+ return;
18505
+ }
18506
+ if (pathname === "/api/sprint-summary" && req.method === "POST") {
18507
+ let bodyStr = "";
18508
+ req.on("data", (chunk) => {
18509
+ bodyStr += chunk;
18510
+ });
18511
+ req.on("end", async () => {
18512
+ try {
18513
+ const { sprintId } = JSON.parse(bodyStr || "{}");
18514
+ const data = getSprintSummaryData(store, sprintId);
18515
+ if (!data) {
18516
+ res.writeHead(404, { "Content-Type": "application/json" });
18517
+ res.end(JSON.stringify({ error: "Sprint not found" }));
18518
+ return;
18519
+ }
18520
+ const summary = await generateSprintSummary(data);
18521
+ const html = renderMarkdown(summary);
18522
+ const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
18523
+ sprintSummaryCache.set(data.sprint.id, { html, generatedAt });
18524
+ res.writeHead(200, { "Content-Type": "application/json" });
18525
+ res.end(JSON.stringify({ summary, html, generatedAt }));
18526
+ } catch (err) {
18527
+ console.error("[marvin web] Sprint summary generation error:", err);
18528
+ res.writeHead(500, { "Content-Type": "application/json" });
18529
+ res.end(JSON.stringify({ error: "Failed to generate summary" }));
18530
+ }
18531
+ });
18532
+ return;
18533
+ }
17812
18534
  const boardMatch = pathname.match(/^\/board(?:\/([^/]+))?$/);
17813
18535
  if (boardMatch) {
17814
18536
  const type = boardMatch[1];
@@ -18338,6 +19060,25 @@ function createReportTools(store) {
18338
19060
  },
18339
19061
  { annotations: { readOnlyHint: true } }
18340
19062
  ),
19063
+ tool8(
19064
+ "generate_sprint_summary",
19065
+ "Generate an AI-powered narrative summary of a sprint's progress, health, achievements, risks, and projected outcome",
19066
+ {
19067
+ sprint: external_exports.string().optional().describe("Sprint ID (e.g. 'SP-001'). Omit for the active sprint.")
19068
+ },
19069
+ async (args) => {
19070
+ const data = collectSprintSummaryData(store, args.sprint);
19071
+ if (!data) {
19072
+ const msg = args.sprint ? `Sprint ${args.sprint} not found.` : "No active sprint found.";
19073
+ return { content: [{ type: "text", text: msg }], isError: true };
19074
+ }
19075
+ const summary = await generateSprintSummary(data);
19076
+ return {
19077
+ content: [{ type: "text", text: summary }]
19078
+ };
19079
+ },
19080
+ { annotations: { readOnlyHint: true } }
19081
+ ),
18341
19082
  tool8(
18342
19083
  "save_report",
18343
19084
  "Save a generated report as a persistent document",
@@ -18770,18 +19511,18 @@ function createContributionTools(store) {
18770
19511
  content: external_exports.string().describe("Contribution content \u2014 the input from the persona"),
18771
19512
  persona: external_exports.string().describe("Persona making the contribution (e.g. 'tech-lead')"),
18772
19513
  contributionType: external_exports.string().describe("Type of contribution (e.g. 'action-result', 'risk-finding')"),
18773
- aboutArtifact: external_exports.string().optional().describe("Artifact ID this contribution relates to (e.g. 'A-001')"),
18774
- status: external_exports.string().optional().describe("Status (default: 'open')"),
19514
+ aboutArtifact: external_exports.string().describe("Artifact ID this contribution relates to (e.g. 'A-001', 'T-003')"),
19515
+ status: external_exports.string().optional().describe("Status (default: 'done')"),
18775
19516
  tags: external_exports.array(external_exports.string()).optional().describe("Tags for categorization")
18776
19517
  },
18777
19518
  async (args) => {
18778
19519
  const frontmatter = {
18779
19520
  title: args.title,
18780
- status: args.status ?? "open",
19521
+ status: args.status ?? "done",
18781
19522
  persona: args.persona,
18782
19523
  contributionType: args.contributionType
18783
19524
  };
18784
- if (args.aboutArtifact) frontmatter.aboutArtifact = args.aboutArtifact;
19525
+ frontmatter.aboutArtifact = args.aboutArtifact;
18785
19526
  if (args.tags) frontmatter.tags = args.tags;
18786
19527
  const doc = store.create("contribution", frontmatter, args.content);
18787
19528
  return {
@@ -19265,6 +20006,7 @@ function createTaskTools(store) {
19265
20006
  title: external_exports.string().describe("Task title"),
19266
20007
  content: external_exports.string().describe("Task description and implementation details"),
19267
20008
  linkedEpic: linkedEpicArray.describe("Epic ID(s) to link this task to (e.g. ['E-001'] or ['E-001', 'E-002'])"),
20009
+ aboutArtifact: external_exports.string().optional().describe("Parent artifact this task derives from (e.g. 'A-001')"),
19268
20010
  status: external_exports.enum(["backlog", "ready", "in-progress", "review", "done"]).optional().describe("Task status (default: 'backlog')"),
19269
20011
  acceptanceCriteria: external_exports.string().optional().describe("Acceptance criteria for the task"),
19270
20012
  technicalNotes: external_exports.string().optional().describe("Technical implementation notes"),
@@ -19290,6 +20032,7 @@ function createTaskTools(store) {
19290
20032
  linkedEpic: linkedEpics,
19291
20033
  tags: [...generateEpicTags(linkedEpics), ...args.tags ?? []]
19292
20034
  };
20035
+ if (args.aboutArtifact) frontmatter.aboutArtifact = args.aboutArtifact;
19293
20036
  if (args.acceptanceCriteria) frontmatter.acceptanceCriteria = args.acceptanceCriteria;
19294
20037
  if (args.technicalNotes) frontmatter.technicalNotes = args.technicalNotes;
19295
20038
  if (args.estimatedPoints !== void 0) frontmatter.estimatedPoints = args.estimatedPoints;
@@ -19313,6 +20056,7 @@ function createTaskTools(store) {
19313
20056
  {
19314
20057
  id: external_exports.string().describe("Task ID to update"),
19315
20058
  title: external_exports.string().optional().describe("New title"),
20059
+ aboutArtifact: external_exports.string().optional().describe("Parent artifact this task derives from (e.g. 'A-001')"),
19316
20060
  status: external_exports.enum(["backlog", "ready", "in-progress", "review", "done"]).optional().describe("New status"),
19317
20061
  content: external_exports.string().optional().describe("New content"),
19318
20062
  linkedEpic: linkedEpicArray.optional().describe("New linked epic ID(s)"),
@@ -21885,6 +22629,24 @@ function createWebTools(store, projectName, navGroups) {
21885
22629
  };
21886
22630
  },
21887
22631
  { annotations: { readOnlyHint: true } }
22632
+ ),
22633
+ tool22(
22634
+ "get_dashboard_sprint_summary",
22635
+ "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.",
22636
+ {
22637
+ sprint: external_exports.string().optional().describe("Sprint ID (e.g. 'SP-001'). Omit for the active sprint.")
22638
+ },
22639
+ async (args) => {
22640
+ const data = getSprintSummaryData(store, args.sprint);
22641
+ if (!data) {
22642
+ const msg = args.sprint ? `Sprint ${args.sprint} not found.` : "No active sprint found.";
22643
+ return { content: [{ type: "text", text: msg }], isError: true };
22644
+ }
22645
+ return {
22646
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
22647
+ };
22648
+ },
22649
+ { annotations: { readOnlyHint: true } }
21888
22650
  )
21889
22651
  ];
21890
22652
  }
@@ -21916,7 +22678,7 @@ import * as readline from "readline";
21916
22678
  import chalk from "chalk";
21917
22679
  import ora from "ora";
21918
22680
  import {
21919
- query as query2
22681
+ query as query3
21920
22682
  } from "@anthropic-ai/claude-agent-sdk";
21921
22683
 
21922
22684
  // src/storage/session-store.ts
@@ -21987,11 +22749,11 @@ var SessionStore = class {
21987
22749
  };
21988
22750
 
21989
22751
  // src/agent/session-namer.ts
21990
- import { query } from "@anthropic-ai/claude-agent-sdk";
22752
+ import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
21991
22753
  async function generateSessionName(turns) {
21992
22754
  try {
21993
22755
  const transcript = turns.slice(-20).map((t) => `${t.role}: ${t.content.slice(0, 200)}`).join("\n");
21994
- const result = query({
22756
+ const result = query2({
21995
22757
  prompt: `Summarize this conversation in 3-5 words as a kebab-case name suitable for a filename. Output ONLY the name, nothing else.
21996
22758
 
21997
22759
  ${transcript}`,
@@ -22258,6 +23020,7 @@ Marvin \u2014 ${persona.name}
22258
23020
  "mcp__marvin-governance__get_dashboard_gar",
22259
23021
  "mcp__marvin-governance__get_dashboard_board",
22260
23022
  "mcp__marvin-governance__get_dashboard_upcoming",
23023
+ "mcp__marvin-governance__get_dashboard_sprint_summary",
22261
23024
  ...pluginTools.map((t) => `mcp__marvin-governance__${t.name}`),
22262
23025
  ...codeSkillTools.map((t) => `mcp__marvin-governance__${t.name}`)
22263
23026
  ]
@@ -22268,7 +23031,7 @@ Marvin \u2014 ${persona.name}
22268
23031
  if (existingSession) {
22269
23032
  queryOptions.resume = existingSession.id;
22270
23033
  }
22271
- const conversation = query2({
23034
+ const conversation = query3({
22272
23035
  prompt,
22273
23036
  options: queryOptions
22274
23037
  });
@@ -22360,7 +23123,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
22360
23123
  import { tool as tool23 } from "@anthropic-ai/claude-agent-sdk";
22361
23124
 
22362
23125
  // src/skills/action-runner.ts
22363
- import { query as query3 } from "@anthropic-ai/claude-agent-sdk";
23126
+ import { query as query4 } from "@anthropic-ai/claude-agent-sdk";
22364
23127
  var GOVERNANCE_TOOL_NAMES2 = [
22365
23128
  "mcp__marvin-governance__list_decisions",
22366
23129
  "mcp__marvin-governance__get_decision",
@@ -22382,7 +23145,7 @@ async function runSkillAction(action, userPrompt, context) {
22382
23145
  try {
22383
23146
  const mcpServer = createMarvinMcpServer(context.store);
22384
23147
  const allowedTools = action.allowGovernanceTools !== false ? GOVERNANCE_TOOL_NAMES2 : [];
22385
- const conversation = query3({
23148
+ const conversation = query4({
22386
23149
  prompt: userPrompt,
22387
23150
  options: {
22388
23151
  systemPrompt: action.systemPrompt,
@@ -23176,7 +23939,7 @@ import * as fs13 from "fs";
23176
23939
  import * as path13 from "path";
23177
23940
  import chalk7 from "chalk";
23178
23941
  import ora2 from "ora";
23179
- import { query as query4 } from "@anthropic-ai/claude-agent-sdk";
23942
+ import { query as query5 } from "@anthropic-ai/claude-agent-sdk";
23180
23943
 
23181
23944
  // src/sources/prompts.ts
23182
23945
  function buildIngestSystemPrompt(persona, projectConfig, isDraft) {
@@ -23309,7 +24072,7 @@ async function ingestFile(options) {
23309
24072
  const spinner = ora2({ text: `Analyzing ${fileName}...`, color: "cyan" });
23310
24073
  spinner.start();
23311
24074
  try {
23312
- const conversation = query4({
24075
+ const conversation = query5({
23313
24076
  prompt: userPrompt,
23314
24077
  options: {
23315
24078
  systemPrompt,
@@ -24530,7 +25293,7 @@ import chalk13 from "chalk";
24530
25293
  // src/analysis/analyze.ts
24531
25294
  import chalk12 from "chalk";
24532
25295
  import ora4 from "ora";
24533
- import { query as query5 } from "@anthropic-ai/claude-agent-sdk";
25296
+ import { query as query6 } from "@anthropic-ai/claude-agent-sdk";
24534
25297
 
24535
25298
  // src/analysis/prompts.ts
24536
25299
  function buildAnalyzeSystemPrompt(persona, projectConfig, isDraft) {
@@ -24660,7 +25423,7 @@ async function analyzeMeeting(options) {
24660
25423
  const spinner = ora4({ text: `Analyzing meeting ${meetingId}...`, color: "cyan" });
24661
25424
  spinner.start();
24662
25425
  try {
24663
- const conversation = query5({
25426
+ const conversation = query6({
24664
25427
  prompt: userPrompt,
24665
25428
  options: {
24666
25429
  systemPrompt,
@@ -24787,7 +25550,7 @@ import chalk15 from "chalk";
24787
25550
  // src/contributions/contribute.ts
24788
25551
  import chalk14 from "chalk";
24789
25552
  import ora5 from "ora";
24790
- import { query as query6 } from "@anthropic-ai/claude-agent-sdk";
25553
+ import { query as query7 } from "@anthropic-ai/claude-agent-sdk";
24791
25554
 
24792
25555
  // src/contributions/prompts.ts
24793
25556
  function buildContributeSystemPrompt(persona, contributionType, projectConfig, isDraft) {
@@ -25041,7 +25804,7 @@ async function contributeFromPersona(options) {
25041
25804
  "mcp__marvin-governance__get_action",
25042
25805
  "mcp__marvin-governance__get_question"
25043
25806
  ];
25044
- const conversation = query6({
25807
+ const conversation = query7({
25045
25808
  prompt: userPrompt,
25046
25809
  options: {
25047
25810
  systemPrompt,
@@ -25187,6 +25950,9 @@ Contribution: ${options.type}`));
25187
25950
  });
25188
25951
  }
25189
25952
 
25953
+ // src/cli/commands/report.ts
25954
+ import ora6 from "ora";
25955
+
25190
25956
  // src/reports/gar/render-ascii.ts
25191
25957
  import chalk16 from "chalk";
25192
25958
  var STATUS_DOT = {
@@ -25381,6 +26147,47 @@ async function healthReportCommand(options) {
25381
26147
  console.log(renderAscii2(report));
25382
26148
  }
25383
26149
  }
26150
+ async function sprintSummaryCommand(options) {
26151
+ const project = loadProject();
26152
+ const plugin = resolvePlugin(project.config.methodology);
26153
+ const pluginRegistrations = plugin?.documentTypeRegistrations ?? [];
26154
+ const allSkills = loadAllSkills(project.marvinDir);
26155
+ const allSkillIds = [...allSkills.keys()];
26156
+ const skillRegistrations = collectSkillRegistrations(allSkillIds, allSkills);
26157
+ const store = new DocumentStore(project.marvinDir, [...pluginRegistrations, ...skillRegistrations]);
26158
+ const data = collectSprintSummaryData(store, options.sprint);
26159
+ if (!data) {
26160
+ const msg = options.sprint ? `Sprint ${options.sprint} not found.` : "No active sprint found. Use --sprint <id> to specify one.";
26161
+ console.error(msg);
26162
+ process.exit(1);
26163
+ }
26164
+ const spinner = ora6({ text: "Generating AI sprint summary...", color: "cyan" }).start();
26165
+ try {
26166
+ const summary = await generateSprintSummary(data);
26167
+ spinner.stop();
26168
+ const header = `# Sprint Summary: ${data.sprint.id} \u2014 ${data.sprint.title}
26169
+
26170
+ `;
26171
+ console.log(header + summary);
26172
+ if (options.save) {
26173
+ const doc = store.create(
26174
+ "report",
26175
+ {
26176
+ title: `Sprint Summary: ${data.sprint.title}`,
26177
+ status: "final",
26178
+ tags: [`report-type:sprint-summary`, `sprint:${data.sprint.id}`]
26179
+ },
26180
+ summary
26181
+ );
26182
+ console.log(`
26183
+ Saved as ${doc.frontmatter.id}`);
26184
+ }
26185
+ } catch (err) {
26186
+ spinner.stop();
26187
+ console.error("Failed to generate sprint summary:", err);
26188
+ process.exit(1);
26189
+ }
26190
+ }
25384
26191
 
25385
26192
  // src/cli/commands/web.ts
25386
26193
  async function webCommand(options) {
@@ -25423,7 +26230,7 @@ function createProgram() {
25423
26230
  const program = new Command();
25424
26231
  program.name("marvin").description(
25425
26232
  "AI-powered product development assistant with Product Owner, Delivery Manager, and Technical Lead personas"
25426
- ).version("0.4.6");
26233
+ ).version("0.4.8");
25427
26234
  program.command("init").description("Initialize a new Marvin project in the current directory").action(async () => {
25428
26235
  await initCommand();
25429
26236
  });
@@ -25506,6 +26313,9 @@ function createProgram() {
25506
26313
  ).action(async (options) => {
25507
26314
  await healthReportCommand(options);
25508
26315
  });
26316
+ 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) => {
26317
+ await sprintSummaryCommand(options);
26318
+ });
25509
26319
  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
26320
  await webCommand(options);
25511
26321
  });