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/marvin.js CHANGED
@@ -14495,6 +14495,346 @@ function evaluateHealth(projectName, metrics) {
14495
14495
  };
14496
14496
  }
14497
14497
 
14498
+ // src/plugins/builtin/tools/task-utils.ts
14499
+ function normalizeLinkedEpics(value) {
14500
+ if (value === void 0 || value === null) return [];
14501
+ if (typeof value === "string") {
14502
+ try {
14503
+ const parsed = JSON.parse(value);
14504
+ if (Array.isArray(parsed)) return parsed.filter((v) => typeof v === "string");
14505
+ } catch {
14506
+ }
14507
+ return [value];
14508
+ }
14509
+ if (Array.isArray(value)) return value.filter((v) => typeof v === "string");
14510
+ return [];
14511
+ }
14512
+ function generateEpicTags(epics) {
14513
+ return epics.map((id) => `epic:${id}`);
14514
+ }
14515
+
14516
+ // src/reports/sprint-summary/collector.ts
14517
+ var DONE_STATUSES = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
14518
+ function collectSprintSummaryData(store, sprintId) {
14519
+ const allDocs = store.list();
14520
+ const sprintDocs = allDocs.filter((d) => d.frontmatter.type === "sprint");
14521
+ let sprintDoc;
14522
+ if (sprintId) {
14523
+ sprintDoc = sprintDocs.find((d) => d.frontmatter.id === sprintId);
14524
+ } else {
14525
+ sprintDoc = sprintDocs.find((d) => d.frontmatter.status === "active");
14526
+ }
14527
+ if (!sprintDoc) return null;
14528
+ const fm = sprintDoc.frontmatter;
14529
+ const startDate = fm.startDate;
14530
+ const endDate = fm.endDate;
14531
+ const today = /* @__PURE__ */ new Date();
14532
+ const todayStr = today.toISOString().slice(0, 10);
14533
+ let daysElapsed = 0;
14534
+ let daysRemaining = 0;
14535
+ let totalDays = 0;
14536
+ let percentComplete = 0;
14537
+ if (startDate && endDate) {
14538
+ const startMs = new Date(startDate).getTime();
14539
+ const endMs = new Date(endDate).getTime();
14540
+ const todayMs = today.getTime();
14541
+ const msPerDay = 864e5;
14542
+ totalDays = Math.max(1, Math.round((endMs - startMs) / msPerDay));
14543
+ daysElapsed = Math.max(0, Math.round((todayMs - startMs) / msPerDay));
14544
+ daysRemaining = Math.max(0, Math.round((endMs - todayMs) / msPerDay));
14545
+ percentComplete = Math.min(100, Math.round(daysElapsed / totalDays * 100));
14546
+ }
14547
+ const linkedEpicIds = normalizeLinkedEpics(fm.linkedEpics);
14548
+ const epicToTasks = /* @__PURE__ */ new Map();
14549
+ const allTasks = allDocs.filter((d) => d.frontmatter.type === "task");
14550
+ for (const task of allTasks) {
14551
+ const tags = task.frontmatter.tags ?? [];
14552
+ for (const tag of tags) {
14553
+ if (tag.startsWith("epic:")) {
14554
+ const epicId = tag.slice(5);
14555
+ if (!epicToTasks.has(epicId)) epicToTasks.set(epicId, []);
14556
+ epicToTasks.get(epicId).push(task);
14557
+ }
14558
+ }
14559
+ }
14560
+ const linkedEpics = linkedEpicIds.map((epicId) => {
14561
+ const epic = store.get(epicId);
14562
+ const tasks = epicToTasks.get(epicId) ?? [];
14563
+ const tasksDone = tasks.filter((t) => DONE_STATUSES.has(t.frontmatter.status)).length;
14564
+ return {
14565
+ id: epicId,
14566
+ title: epic?.frontmatter.title ?? "(not found)",
14567
+ status: epic?.frontmatter.status ?? "unknown",
14568
+ tasksDone,
14569
+ tasksTotal: tasks.length
14570
+ };
14571
+ });
14572
+ const sprintTag = `sprint:${fm.id}`;
14573
+ const workItemDocs = allDocs.filter(
14574
+ (d) => d.frontmatter.type !== "sprint" && d.frontmatter.type !== "epic" && d.frontmatter.tags?.includes(sprintTag)
14575
+ );
14576
+ const byStatus = {};
14577
+ const byType = {};
14578
+ let doneCount = 0;
14579
+ let inProgressCount = 0;
14580
+ let openCount = 0;
14581
+ let blockedCount = 0;
14582
+ for (const doc of workItemDocs) {
14583
+ const s = doc.frontmatter.status;
14584
+ byStatus[s] = (byStatus[s] ?? 0) + 1;
14585
+ byType[doc.frontmatter.type] = (byType[doc.frontmatter.type] ?? 0) + 1;
14586
+ if (DONE_STATUSES.has(s)) doneCount++;
14587
+ else if (s === "in-progress") inProgressCount++;
14588
+ else if (s === "blocked") blockedCount++;
14589
+ else openCount++;
14590
+ }
14591
+ const workItems = {
14592
+ total: workItemDocs.length,
14593
+ done: doneCount,
14594
+ inProgress: inProgressCount,
14595
+ open: openCount,
14596
+ blocked: blockedCount,
14597
+ completionPct: workItemDocs.length > 0 ? Math.round(doneCount / workItemDocs.length * 100) : 0,
14598
+ byStatus,
14599
+ byType,
14600
+ items: workItemDocs.map((d) => ({
14601
+ id: d.frontmatter.id,
14602
+ title: d.frontmatter.title,
14603
+ type: d.frontmatter.type,
14604
+ status: d.frontmatter.status
14605
+ }))
14606
+ };
14607
+ const meetings = [];
14608
+ if (startDate && endDate) {
14609
+ const meetingDocs = allDocs.filter((d) => d.frontmatter.type === "meeting");
14610
+ for (const m of meetingDocs) {
14611
+ const meetingDate = m.frontmatter.date ?? m.frontmatter.created.slice(0, 10);
14612
+ if (meetingDate >= startDate && meetingDate <= endDate) {
14613
+ meetings.push({
14614
+ id: m.frontmatter.id,
14615
+ title: m.frontmatter.title,
14616
+ date: meetingDate
14617
+ });
14618
+ }
14619
+ }
14620
+ meetings.sort((a, b) => a.date.localeCompare(b.date));
14621
+ }
14622
+ const artifacts = [];
14623
+ if (startDate && endDate) {
14624
+ for (const doc of allDocs) {
14625
+ if (doc.frontmatter.type === "sprint") continue;
14626
+ const created = doc.frontmatter.created.slice(0, 10);
14627
+ const updated = doc.frontmatter.updated.slice(0, 10);
14628
+ if (created >= startDate && created <= endDate) {
14629
+ artifacts.push({
14630
+ id: doc.frontmatter.id,
14631
+ title: doc.frontmatter.title,
14632
+ type: doc.frontmatter.type,
14633
+ action: "created",
14634
+ date: created
14635
+ });
14636
+ } else if (updated >= startDate && updated <= endDate && updated !== created) {
14637
+ artifacts.push({
14638
+ id: doc.frontmatter.id,
14639
+ title: doc.frontmatter.title,
14640
+ type: doc.frontmatter.type,
14641
+ action: "updated",
14642
+ date: updated
14643
+ });
14644
+ }
14645
+ }
14646
+ artifacts.sort((a, b) => b.date.localeCompare(a.date));
14647
+ }
14648
+ const relevantTags = /* @__PURE__ */ new Set([sprintTag, ...linkedEpicIds.map((id) => `epic:${id}`)]);
14649
+ const openActions = allDocs.filter(
14650
+ (d) => d.frontmatter.type === "action" && !DONE_STATUSES.has(d.frontmatter.status) && d.frontmatter.tags?.some((t) => relevantTags.has(t))
14651
+ ).map((d) => ({
14652
+ id: d.frontmatter.id,
14653
+ title: d.frontmatter.title,
14654
+ owner: d.frontmatter.owner,
14655
+ dueDate: d.frontmatter.dueDate
14656
+ }));
14657
+ const openQuestions = allDocs.filter(
14658
+ (d) => d.frontmatter.type === "question" && d.frontmatter.status === "open" && d.frontmatter.tags?.some((t) => relevantTags.has(t))
14659
+ ).map((d) => ({
14660
+ id: d.frontmatter.id,
14661
+ title: d.frontmatter.title
14662
+ }));
14663
+ const blockers = allDocs.filter(
14664
+ (d) => d.frontmatter.status === "blocked" && d.frontmatter.tags?.includes(sprintTag)
14665
+ ).map((d) => ({
14666
+ id: d.frontmatter.id,
14667
+ title: d.frontmatter.title,
14668
+ type: d.frontmatter.type
14669
+ }));
14670
+ const riskBlockers = allDocs.filter(
14671
+ (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)
14672
+ );
14673
+ for (const d of riskBlockers) {
14674
+ blockers.push({
14675
+ id: d.frontmatter.id,
14676
+ title: d.frontmatter.title,
14677
+ type: d.frontmatter.type
14678
+ });
14679
+ }
14680
+ let velocity = null;
14681
+ const currentRate = workItems.completionPct;
14682
+ 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 ?? ""));
14683
+ if (completedSprints.length > 0) {
14684
+ const prev = completedSprints[0];
14685
+ const prevTag = `sprint:${prev.frontmatter.id}`;
14686
+ const prevWorkItems = allDocs.filter(
14687
+ (d) => d.frontmatter.type !== "sprint" && d.frontmatter.type !== "epic" && d.frontmatter.tags?.includes(prevTag)
14688
+ );
14689
+ const prevDone = prevWorkItems.filter((d) => DONE_STATUSES.has(d.frontmatter.status)).length;
14690
+ const prevRate = prevWorkItems.length > 0 ? Math.round(prevDone / prevWorkItems.length * 100) : 0;
14691
+ velocity = {
14692
+ currentCompletionRate: currentRate,
14693
+ previousSprintRate: prevRate,
14694
+ previousSprintId: prev.frontmatter.id
14695
+ };
14696
+ } else {
14697
+ velocity = { currentCompletionRate: currentRate };
14698
+ }
14699
+ return {
14700
+ sprint: {
14701
+ id: fm.id,
14702
+ title: fm.title,
14703
+ goal: fm.goal,
14704
+ status: fm.status,
14705
+ startDate,
14706
+ endDate
14707
+ },
14708
+ timeline: { daysElapsed, daysRemaining, totalDays, percentComplete },
14709
+ linkedEpics,
14710
+ workItems,
14711
+ meetings,
14712
+ artifacts,
14713
+ openActions,
14714
+ openQuestions,
14715
+ blockers,
14716
+ velocity
14717
+ };
14718
+ }
14719
+
14720
+ // src/reports/sprint-summary/generator.ts
14721
+ import { query } from "@anthropic-ai/claude-agent-sdk";
14722
+ async function generateSprintSummary(data) {
14723
+ const prompt = buildPrompt(data);
14724
+ const result = query({
14725
+ prompt,
14726
+ options: {
14727
+ systemPrompt: SYSTEM_PROMPT,
14728
+ maxTurns: 1,
14729
+ tools: [],
14730
+ allowedTools: []
14731
+ }
14732
+ });
14733
+ for await (const msg of result) {
14734
+ if (msg.type === "assistant") {
14735
+ const text = msg.message.content.find(
14736
+ (b) => b.type === "text"
14737
+ );
14738
+ if (text) return text.text;
14739
+ }
14740
+ }
14741
+ return "Unable to generate sprint summary.";
14742
+ }
14743
+ 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:
14744
+
14745
+ ## Sprint Health
14746
+ One-line verdict on overall sprint health (healthy / at risk / behind).
14747
+
14748
+ ## Goal Progress
14749
+ How close the team is to achieving the sprint goal. Reference the goal text and completion metrics.
14750
+
14751
+ ## Key Achievements
14752
+ Notable completions, decisions made, meetings held during the sprint. Use bullet points.
14753
+
14754
+ ## Current Risks
14755
+ Blockers, overdue items, unresolved questions, items without owners. Use bullet points. If none, say so.
14756
+
14757
+ ## Outcome Projection
14758
+ Given the current pace and time remaining, what's the likely outcome? Will the sprint goal be met?
14759
+
14760
+ Be specific \u2014 reference artifact IDs, dates, and numbers from the data. Keep the tone professional but direct.`;
14761
+ function buildPrompt(data) {
14762
+ const sections = [];
14763
+ sections.push(`# Sprint: ${data.sprint.id} \u2014 ${data.sprint.title}`);
14764
+ sections.push(`Status: ${data.sprint.status}`);
14765
+ if (data.sprint.goal) sections.push(`Goal: ${data.sprint.goal}`);
14766
+ if (data.sprint.startDate) sections.push(`Start: ${data.sprint.startDate}`);
14767
+ if (data.sprint.endDate) sections.push(`End: ${data.sprint.endDate}`);
14768
+ sections.push(`
14769
+ ## Timeline`);
14770
+ sections.push(`Days elapsed: ${data.timeline.daysElapsed} / ${data.timeline.totalDays}`);
14771
+ sections.push(`Days remaining: ${data.timeline.daysRemaining}`);
14772
+ sections.push(`Timeline progress: ${data.timeline.percentComplete}%`);
14773
+ sections.push(`
14774
+ ## Work Items`);
14775
+ sections.push(`Total: ${data.workItems.total}, Done: ${data.workItems.done}, In Progress: ${data.workItems.inProgress}, Open: ${data.workItems.open}, Blocked: ${data.workItems.blocked}`);
14776
+ sections.push(`Completion: ${data.workItems.completionPct}%`);
14777
+ if (Object.keys(data.workItems.byType).length > 0) {
14778
+ sections.push(`By type: ${Object.entries(data.workItems.byType).map(([t, n]) => `${t}: ${n}`).join(", ")}`);
14779
+ }
14780
+ if (data.linkedEpics.length > 0) {
14781
+ sections.push(`
14782
+ ## Linked Epics`);
14783
+ for (const e of data.linkedEpics) {
14784
+ sections.push(`- ${e.id}: ${e.title} [${e.status}] \u2014 ${e.tasksDone}/${e.tasksTotal} tasks done`);
14785
+ }
14786
+ }
14787
+ if (data.meetings.length > 0) {
14788
+ sections.push(`
14789
+ ## Meetings During Sprint`);
14790
+ for (const m of data.meetings) {
14791
+ sections.push(`- ${m.date}: ${m.id} \u2014 ${m.title}`);
14792
+ }
14793
+ }
14794
+ if (data.artifacts.length > 0) {
14795
+ sections.push(`
14796
+ ## Artifacts Created/Updated During Sprint`);
14797
+ for (const a of data.artifacts.slice(0, 20)) {
14798
+ sections.push(`- ${a.date}: ${a.id} (${a.type}) ${a.action} \u2014 ${a.title}`);
14799
+ }
14800
+ if (data.artifacts.length > 20) {
14801
+ sections.push(`... and ${data.artifacts.length - 20} more`);
14802
+ }
14803
+ }
14804
+ if (data.openActions.length > 0) {
14805
+ sections.push(`
14806
+ ## Open Actions`);
14807
+ for (const a of data.openActions) {
14808
+ const owner = a.owner ?? "unowned";
14809
+ const due = a.dueDate ?? "no due date";
14810
+ sections.push(`- ${a.id}: ${a.title} (${owner}, ${due})`);
14811
+ }
14812
+ }
14813
+ if (data.openQuestions.length > 0) {
14814
+ sections.push(`
14815
+ ## Open Questions`);
14816
+ for (const q of data.openQuestions) {
14817
+ sections.push(`- ${q.id}: ${q.title}`);
14818
+ }
14819
+ }
14820
+ if (data.blockers.length > 0) {
14821
+ sections.push(`
14822
+ ## Blockers`);
14823
+ for (const b of data.blockers) {
14824
+ sections.push(`- ${b.id} (${b.type}): ${b.title}`);
14825
+ }
14826
+ }
14827
+ if (data.velocity) {
14828
+ sections.push(`
14829
+ ## Velocity`);
14830
+ sections.push(`Current sprint completion rate: ${data.velocity.currentCompletionRate}%`);
14831
+ if (data.velocity.previousSprintRate !== void 0) {
14832
+ sections.push(`Previous sprint (${data.velocity.previousSprintId}): ${data.velocity.previousSprintRate}%`);
14833
+ }
14834
+ }
14835
+ return sections.join("\n");
14836
+ }
14837
+
14498
14838
  // src/plugins/builtin/tools/reports.ts
14499
14839
  function createReportTools(store) {
14500
14840
  return [
@@ -14786,6 +15126,25 @@ function createReportTools(store) {
14786
15126
  },
14787
15127
  { annotations: { readOnlyHint: true } }
14788
15128
  ),
15129
+ tool2(
15130
+ "generate_sprint_summary",
15131
+ "Generate an AI-powered narrative summary of a sprint's progress, health, achievements, risks, and projected outcome",
15132
+ {
15133
+ sprint: external_exports.string().optional().describe("Sprint ID (e.g. 'SP-001'). Omit for the active sprint.")
15134
+ },
15135
+ async (args) => {
15136
+ const data = collectSprintSummaryData(store, args.sprint);
15137
+ if (!data) {
15138
+ const msg = args.sprint ? `Sprint ${args.sprint} not found.` : "No active sprint found.";
15139
+ return { content: [{ type: "text", text: msg }], isError: true };
15140
+ }
15141
+ const summary = await generateSprintSummary(data);
15142
+ return {
15143
+ content: [{ type: "text", text: summary }]
15144
+ };
15145
+ },
15146
+ { annotations: { readOnlyHint: true } }
15147
+ ),
14789
15148
  tool2(
14790
15149
  "save_report",
14791
15150
  "Save a generated report as a persistent document",
@@ -15629,26 +15988,6 @@ function createSprintPlanningTools(store) {
15629
15988
 
15630
15989
  // src/plugins/builtin/tools/tasks.ts
15631
15990
  import { tool as tool8 } from "@anthropic-ai/claude-agent-sdk";
15632
-
15633
- // src/plugins/builtin/tools/task-utils.ts
15634
- function normalizeLinkedEpics(value) {
15635
- if (value === void 0 || value === null) return [];
15636
- if (typeof value === "string") {
15637
- try {
15638
- const parsed = JSON.parse(value);
15639
- if (Array.isArray(parsed)) return parsed.filter((v) => typeof v === "string");
15640
- } catch {
15641
- }
15642
- return [value];
15643
- }
15644
- if (Array.isArray(value)) return value.filter((v) => typeof v === "string");
15645
- return [];
15646
- }
15647
- function generateEpicTags(epics) {
15648
- return epics.map((id) => `epic:${id}`);
15649
- }
15650
-
15651
- // src/plugins/builtin/tools/tasks.ts
15652
15991
  var linkedEpicArray = external_exports.preprocess(
15653
15992
  (val) => {
15654
15993
  if (typeof val === "string") {
@@ -17243,7 +17582,7 @@ import * as readline from "readline";
17243
17582
  import chalk2 from "chalk";
17244
17583
  import ora from "ora";
17245
17584
  import {
17246
- query as query2
17585
+ query as query3
17247
17586
  } from "@anthropic-ai/claude-agent-sdk";
17248
17587
 
17249
17588
  // src/personas/prompt-builder.ts
@@ -17383,9 +17722,9 @@ var DocumentStore = class {
17383
17722
  }
17384
17723
  }
17385
17724
  }
17386
- list(query7) {
17725
+ list(query8) {
17387
17726
  const results = [];
17388
- const types = query7?.type ? [query7.type] : Object.keys(this.typeDirs);
17727
+ const types = query8?.type ? [query8.type] : Object.keys(this.typeDirs);
17389
17728
  for (const type of types) {
17390
17729
  const dirName = this.typeDirs[type];
17391
17730
  if (!dirName) continue;
@@ -17396,9 +17735,9 @@ var DocumentStore = class {
17396
17735
  const filePath = path6.join(dir, file2);
17397
17736
  const raw = fs6.readFileSync(filePath, "utf-8");
17398
17737
  const doc = parseDocument(raw, filePath);
17399
- if (query7?.status && doc.frontmatter.status !== query7.status) continue;
17400
- if (query7?.owner && doc.frontmatter.owner !== query7.owner) continue;
17401
- if (query7?.tag && (!doc.frontmatter.tags || !doc.frontmatter.tags.includes(query7.tag)))
17738
+ if (query8?.status && doc.frontmatter.status !== query8.status) continue;
17739
+ if (query8?.owner && doc.frontmatter.owner !== query8.owner) continue;
17740
+ if (query8?.tag && (!doc.frontmatter.tags || !doc.frontmatter.tags.includes(query8.tag)))
17402
17741
  continue;
17403
17742
  results.push(doc);
17404
17743
  }
@@ -18364,7 +18703,7 @@ function computeUrgency(dueDateStr, todayStr) {
18364
18703
  if (diffDays <= 14) return "upcoming";
18365
18704
  return "later";
18366
18705
  }
18367
- var DONE_STATUSES = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
18706
+ var DONE_STATUSES2 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
18368
18707
  function getUpcomingData(store) {
18369
18708
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
18370
18709
  const allDocs = store.list();
@@ -18373,7 +18712,7 @@ function getUpcomingData(store) {
18373
18712
  docById.set(doc.frontmatter.id, doc);
18374
18713
  }
18375
18714
  const actions = allDocs.filter(
18376
- (d) => d.frontmatter.type === "action" && !DONE_STATUSES.has(d.frontmatter.status)
18715
+ (d) => d.frontmatter.type === "action" && !DONE_STATUSES2.has(d.frontmatter.status)
18377
18716
  );
18378
18717
  const actionsWithDue = actions.filter((d) => d.frontmatter.dueDate);
18379
18718
  const sprints = allDocs.filter((d) => d.frontmatter.type === "sprint");
@@ -18437,7 +18776,7 @@ function getUpcomingData(store) {
18437
18776
  const sprintEnd = sprint.frontmatter.endDate;
18438
18777
  const sprintTaskDocs = getSprintTasks(sprint);
18439
18778
  for (const task of sprintTaskDocs) {
18440
- if (DONE_STATUSES.has(task.frontmatter.status)) continue;
18779
+ if (DONE_STATUSES2.has(task.frontmatter.status)) continue;
18441
18780
  const existing = taskSprintMap.get(task.frontmatter.id);
18442
18781
  if (!existing || sprintEnd < existing.sprintEnd) {
18443
18782
  taskSprintMap.set(task.frontmatter.id, { task, sprint, sprintEnd });
@@ -18454,7 +18793,7 @@ function getUpcomingData(store) {
18454
18793
  urgency: computeUrgency(sprintEnd, today)
18455
18794
  })).sort((a, b) => a.sprintEndDate.localeCompare(b.sprintEndDate));
18456
18795
  const openItems = allDocs.filter(
18457
- (d) => ["action", "question", "task"].includes(d.frontmatter.type) && !DONE_STATUSES.has(d.frontmatter.status)
18796
+ (d) => ["action", "question", "task"].includes(d.frontmatter.type) && !DONE_STATUSES2.has(d.frontmatter.status)
18458
18797
  );
18459
18798
  const fourteenDaysAgo = new Date(todayMs - fourteenDaysMs).toISOString().slice(0, 10);
18460
18799
  const recentMeetings = allDocs.filter(
@@ -18552,6 +18891,9 @@ function getUpcomingData(store) {
18552
18891
  }).filter((item) => item.score > 0).sort((a, b) => b.score - a.score).slice(0, 15);
18553
18892
  return { dueSoonActions, dueSoonSprintTasks, trending };
18554
18893
  }
18894
+ function getSprintSummaryData(store, sprintId) {
18895
+ return collectSprintSummaryData(store, sprintId);
18896
+ }
18555
18897
 
18556
18898
  // src/web/templates/layout.ts
18557
18899
  function collapsibleSection(sectionId, title, content, opts) {
@@ -18693,6 +19035,7 @@ function layout(opts, body) {
18693
19035
  const topItems = [
18694
19036
  { href: "/", label: "Overview" },
18695
19037
  { href: "/upcoming", label: "Upcoming" },
19038
+ { href: "/sprint-summary", label: "Sprint Summary" },
18696
19039
  { href: "/timeline", label: "Timeline" },
18697
19040
  { href: "/board", label: "Board" },
18698
19041
  { href: "/gar", label: "GAR Report" },
@@ -19703,6 +20046,112 @@ tr:hover td {
19703
20046
 
19704
20047
  .text-dim { color: var(--text-dim); }
19705
20048
 
20049
+ /* Sprint Summary */
20050
+ .sprint-goal {
20051
+ background: var(--bg-card);
20052
+ border: 1px solid var(--border);
20053
+ border-radius: var(--radius);
20054
+ padding: 0.75rem 1rem;
20055
+ margin-bottom: 1rem;
20056
+ font-size: 0.9rem;
20057
+ color: var(--text);
20058
+ }
20059
+
20060
+ .sprint-progress-bar {
20061
+ position: relative;
20062
+ height: 24px;
20063
+ background: var(--bg-card);
20064
+ border: 1px solid var(--border);
20065
+ border-radius: 12px;
20066
+ margin-bottom: 1.25rem;
20067
+ overflow: hidden;
20068
+ }
20069
+
20070
+ .sprint-progress-fill {
20071
+ height: 100%;
20072
+ background: linear-gradient(90deg, var(--accent-dim), var(--accent));
20073
+ border-radius: 12px;
20074
+ transition: width 0.3s ease;
20075
+ }
20076
+
20077
+ .sprint-progress-label {
20078
+ position: absolute;
20079
+ top: 50%;
20080
+ left: 50%;
20081
+ transform: translate(-50%, -50%);
20082
+ font-size: 0.7rem;
20083
+ font-weight: 700;
20084
+ color: var(--text);
20085
+ }
20086
+
20087
+ .sprint-ai-section {
20088
+ margin-top: 2rem;
20089
+ background: var(--bg-card);
20090
+ border: 1px solid var(--border);
20091
+ border-radius: var(--radius);
20092
+ padding: 1.5rem;
20093
+ }
20094
+
20095
+ .sprint-ai-section h3 {
20096
+ font-size: 1rem;
20097
+ font-weight: 600;
20098
+ margin-bottom: 0.5rem;
20099
+ }
20100
+
20101
+ .sprint-generate-btn {
20102
+ background: var(--accent);
20103
+ color: #fff;
20104
+ border: none;
20105
+ border-radius: var(--radius);
20106
+ padding: 0.5rem 1.25rem;
20107
+ font-size: 0.85rem;
20108
+ font-weight: 600;
20109
+ cursor: pointer;
20110
+ margin-top: 0.75rem;
20111
+ transition: background 0.15s;
20112
+ }
20113
+
20114
+ .sprint-generate-btn:hover:not(:disabled) {
20115
+ background: var(--accent-dim);
20116
+ }
20117
+
20118
+ .sprint-generate-btn:disabled {
20119
+ opacity: 0.5;
20120
+ cursor: not-allowed;
20121
+ }
20122
+
20123
+ .sprint-loading {
20124
+ display: flex;
20125
+ align-items: center;
20126
+ gap: 0.75rem;
20127
+ padding: 1rem 0;
20128
+ color: var(--text-dim);
20129
+ font-size: 0.85rem;
20130
+ }
20131
+
20132
+ .sprint-spinner {
20133
+ width: 20px;
20134
+ height: 20px;
20135
+ border: 2px solid var(--border);
20136
+ border-top-color: var(--accent);
20137
+ border-radius: 50%;
20138
+ animation: sprint-spin 0.8s linear infinite;
20139
+ }
20140
+
20141
+ @keyframes sprint-spin {
20142
+ to { transform: rotate(360deg); }
20143
+ }
20144
+
20145
+ .sprint-error {
20146
+ color: var(--red);
20147
+ font-size: 0.85rem;
20148
+ padding: 0.5rem 0;
20149
+ }
20150
+
20151
+ .sprint-ai-section .detail-content {
20152
+ margin-top: 1rem;
20153
+ }
20154
+
19706
20155
  /* Collapsible sections */
19707
20156
  .collapsible-header {
19708
20157
  cursor: pointer;
@@ -20594,7 +21043,199 @@ function upcomingPage(data) {
20594
21043
  `;
20595
21044
  }
20596
21045
 
21046
+ // src/web/templates/pages/sprint-summary.ts
21047
+ function progressBar(pct) {
21048
+ return `<div class="sprint-progress-bar">
21049
+ <div class="sprint-progress-fill" style="width: ${pct}%"></div>
21050
+ <span class="sprint-progress-label">${pct}%</span>
21051
+ </div>`;
21052
+ }
21053
+ function sprintSummaryPage(data, cached2) {
21054
+ if (!data) {
21055
+ return `
21056
+ <div class="page-header">
21057
+ <h2>Sprint Summary</h2>
21058
+ <div class="subtitle">AI-powered sprint narrative</div>
21059
+ </div>
21060
+ <div class="empty">
21061
+ <h3>No Active Sprint</h3>
21062
+ <p>No active sprint found. Create a sprint and set its status to "active" to see the summary.</p>
21063
+ </div>`;
21064
+ }
21065
+ const statsCards = `
21066
+ <div class="cards">
21067
+ <div class="card">
21068
+ <div class="card-label">Completion</div>
21069
+ <div class="card-value">${data.workItems.completionPct}%</div>
21070
+ <div class="card-sub">${data.workItems.done} / ${data.workItems.total} items done</div>
21071
+ </div>
21072
+ <div class="card">
21073
+ <div class="card-label">Days Remaining</div>
21074
+ <div class="card-value">${data.timeline.daysRemaining}</div>
21075
+ <div class="card-sub">${data.timeline.daysElapsed} of ${data.timeline.totalDays} elapsed</div>
21076
+ </div>
21077
+ <div class="card">
21078
+ <div class="card-label">Epics</div>
21079
+ <div class="card-value">${data.linkedEpics.length}</div>
21080
+ <div class="card-sub">linked to sprint</div>
21081
+ </div>
21082
+ <div class="card">
21083
+ <div class="card-label">Blockers</div>
21084
+ <div class="card-value${data.blockers.length > 0 ? " priority-high" : ""}">${data.blockers.length}</div>
21085
+ <div class="card-sub">${data.openActions.length} open actions</div>
21086
+ </div>
21087
+ </div>`;
21088
+ const epicsTable = data.linkedEpics.length > 0 ? collapsibleSection(
21089
+ "ss-epics",
21090
+ "Linked Epics",
21091
+ `<div class="table-wrap">
21092
+ <table>
21093
+ <thead>
21094
+ <tr><th>ID</th><th>Title</th><th>Status</th><th>Tasks</th></tr>
21095
+ </thead>
21096
+ <tbody>
21097
+ ${data.linkedEpics.map((e) => `
21098
+ <tr>
21099
+ <td><a href="/docs/epic/${escapeHtml(e.id)}">${escapeHtml(e.id)}</a></td>
21100
+ <td>${escapeHtml(e.title)}</td>
21101
+ <td>${statusBadge(e.status)}</td>
21102
+ <td>${e.tasksDone} / ${e.tasksTotal}</td>
21103
+ </tr>`).join("")}
21104
+ </tbody>
21105
+ </table>
21106
+ </div>`,
21107
+ { titleTag: "h3" }
21108
+ ) : "";
21109
+ const workItemsSection = data.workItems.total > 0 ? collapsibleSection(
21110
+ "ss-work-items",
21111
+ "Work Items",
21112
+ `<div class="table-wrap">
21113
+ <table>
21114
+ <thead>
21115
+ <tr><th>ID</th><th>Title</th><th>Type</th><th>Status</th></tr>
21116
+ </thead>
21117
+ <tbody>
21118
+ ${data.workItems.items.map((w) => `
21119
+ <tr>
21120
+ <td><a href="/docs/${escapeHtml(w.type)}/${escapeHtml(w.id)}">${escapeHtml(w.id)}</a></td>
21121
+ <td>${escapeHtml(w.title)}</td>
21122
+ <td>${escapeHtml(typeLabel(w.type))}</td>
21123
+ <td>${statusBadge(w.status)}</td>
21124
+ </tr>`).join("")}
21125
+ </tbody>
21126
+ </table>
21127
+ </div>`,
21128
+ { titleTag: "h3", defaultCollapsed: true }
21129
+ ) : "";
21130
+ const activitySection = data.artifacts.length > 0 ? collapsibleSection(
21131
+ "ss-activity",
21132
+ "Recent Activity",
21133
+ `<div class="table-wrap">
21134
+ <table>
21135
+ <thead>
21136
+ <tr><th>Date</th><th>ID</th><th>Title</th><th>Type</th><th>Action</th></tr>
21137
+ </thead>
21138
+ <tbody>
21139
+ ${data.artifacts.slice(0, 15).map((a) => `
21140
+ <tr>
21141
+ <td>${formatDate(a.date)}</td>
21142
+ <td><a href="/docs/${escapeHtml(a.type)}/${escapeHtml(a.id)}">${escapeHtml(a.id)}</a></td>
21143
+ <td>${escapeHtml(a.title)}</td>
21144
+ <td>${escapeHtml(typeLabel(a.type))}</td>
21145
+ <td>${escapeHtml(a.action)}</td>
21146
+ </tr>`).join("")}
21147
+ </tbody>
21148
+ </table>
21149
+ </div>`,
21150
+ { titleTag: "h3", defaultCollapsed: true }
21151
+ ) : "";
21152
+ const meetingsSection = data.meetings.length > 0 ? collapsibleSection(
21153
+ "ss-meetings",
21154
+ `Meetings (${data.meetings.length})`,
21155
+ `<div class="table-wrap">
21156
+ <table>
21157
+ <thead>
21158
+ <tr><th>Date</th><th>ID</th><th>Title</th></tr>
21159
+ </thead>
21160
+ <tbody>
21161
+ ${data.meetings.map((m) => `
21162
+ <tr>
21163
+ <td>${formatDate(m.date)}</td>
21164
+ <td><a href="/docs/meeting/${escapeHtml(m.id)}">${escapeHtml(m.id)}</a></td>
21165
+ <td>${escapeHtml(m.title)}</td>
21166
+ </tr>`).join("")}
21167
+ </tbody>
21168
+ </table>
21169
+ </div>`,
21170
+ { titleTag: "h3", defaultCollapsed: true }
21171
+ ) : "";
21172
+ const goalHtml = data.sprint.goal ? `<div class="sprint-goal"><strong>Goal:</strong> ${escapeHtml(data.sprint.goal)}</div>` : "";
21173
+ const dateRange = data.sprint.startDate && data.sprint.endDate ? `<span class="text-dim">${formatDate(data.sprint.startDate)} \u2014 ${formatDate(data.sprint.endDate)}</span>` : "";
21174
+ return `
21175
+ <div class="page-header">
21176
+ <h2>${escapeHtml(data.sprint.id)} \u2014 ${escapeHtml(data.sprint.title)} ${statusBadge(data.sprint.status)}</h2>
21177
+ <div class="subtitle">Sprint Summary ${dateRange}</div>
21178
+ </div>
21179
+ ${goalHtml}
21180
+ ${progressBar(data.timeline.percentComplete)}
21181
+ ${statsCards}
21182
+ ${epicsTable}
21183
+ ${workItemsSection}
21184
+ ${activitySection}
21185
+ ${meetingsSection}
21186
+
21187
+ <div class="sprint-ai-section">
21188
+ <h3>AI Summary</h3>
21189
+ ${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>`}
21190
+ <button class="sprint-generate-btn" onclick="generateSummary()" id="generate-btn">${cached2 ? "Regenerate" : "Generate AI Summary"}</button>
21191
+ <div id="summary-loading" class="sprint-loading" style="display:none">
21192
+ <div class="sprint-spinner"></div>
21193
+ <span>Generating summary...</span>
21194
+ </div>
21195
+ <div id="summary-error" class="sprint-error" style="display:none"></div>
21196
+ <div id="summary-content" class="detail-content"${cached2 ? "" : ' style="display:none"'}>${cached2 ? cached2.html : ""}</div>
21197
+ </div>
21198
+
21199
+ <script>
21200
+ async function generateSummary() {
21201
+ var btn = document.getElementById('generate-btn');
21202
+ var loading = document.getElementById('summary-loading');
21203
+ var errorEl = document.getElementById('summary-error');
21204
+ var content = document.getElementById('summary-content');
21205
+
21206
+ btn.disabled = true;
21207
+ btn.style.display = 'none';
21208
+ loading.style.display = 'flex';
21209
+ errorEl.style.display = 'none';
21210
+ content.style.display = 'none';
21211
+
21212
+ try {
21213
+ var res = await fetch('/api/sprint-summary', {
21214
+ method: 'POST',
21215
+ headers: { 'Content-Type': 'application/json' },
21216
+ body: JSON.stringify({ sprintId: '${escapeHtml(data.sprint.id)}' })
21217
+ });
21218
+ var json = await res.json();
21219
+ if (!res.ok) throw new Error(json.error || 'Failed to generate summary');
21220
+ loading.style.display = 'none';
21221
+ content.innerHTML = json.html;
21222
+ content.style.display = 'block';
21223
+ btn.textContent = 'Regenerate';
21224
+ btn.style.display = '';
21225
+ btn.disabled = false;
21226
+ } catch (e) {
21227
+ loading.style.display = 'none';
21228
+ errorEl.textContent = e.message;
21229
+ errorEl.style.display = 'block';
21230
+ btn.style.display = '';
21231
+ btn.disabled = false;
21232
+ }
21233
+ }
21234
+ </script>`;
21235
+ }
21236
+
20597
21237
  // src/web/router.ts
21238
+ var sprintSummaryCache = /* @__PURE__ */ new Map();
20598
21239
  function handleRequest(req, res, store, projectName, navGroups) {
20599
21240
  const parsed = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
20600
21241
  const pathname = parsed.pathname;
@@ -20640,6 +21281,42 @@ function handleRequest(req, res, store, projectName, navGroups) {
20640
21281
  respond(res, layout({ title: "Upcoming", activePath: "/upcoming", projectName, navGroups }, body));
20641
21282
  return;
20642
21283
  }
21284
+ if (pathname === "/sprint-summary" && req.method === "GET") {
21285
+ const sprintId = parsed.searchParams.get("sprint") ?? void 0;
21286
+ const data = getSprintSummaryData(store, sprintId);
21287
+ const cached2 = data ? sprintSummaryCache.get(data.sprint.id) : void 0;
21288
+ const body = sprintSummaryPage(data, cached2 ? { html: cached2.html, generatedAt: cached2.generatedAt } : void 0);
21289
+ respond(res, layout({ title: "Sprint Summary", activePath: "/sprint-summary", projectName, navGroups }, body));
21290
+ return;
21291
+ }
21292
+ if (pathname === "/api/sprint-summary" && req.method === "POST") {
21293
+ let bodyStr = "";
21294
+ req.on("data", (chunk) => {
21295
+ bodyStr += chunk;
21296
+ });
21297
+ req.on("end", async () => {
21298
+ try {
21299
+ const { sprintId } = JSON.parse(bodyStr || "{}");
21300
+ const data = getSprintSummaryData(store, sprintId);
21301
+ if (!data) {
21302
+ res.writeHead(404, { "Content-Type": "application/json" });
21303
+ res.end(JSON.stringify({ error: "Sprint not found" }));
21304
+ return;
21305
+ }
21306
+ const summary = await generateSprintSummary(data);
21307
+ const html = renderMarkdown(summary);
21308
+ const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
21309
+ sprintSummaryCache.set(data.sprint.id, { html, generatedAt });
21310
+ res.writeHead(200, { "Content-Type": "application/json" });
21311
+ res.end(JSON.stringify({ summary, html, generatedAt }));
21312
+ } catch (err) {
21313
+ console.error("[marvin web] Sprint summary generation error:", err);
21314
+ res.writeHead(500, { "Content-Type": "application/json" });
21315
+ res.end(JSON.stringify({ error: "Failed to generate summary" }));
21316
+ }
21317
+ });
21318
+ return;
21319
+ }
20643
21320
  const boardMatch = pathname.match(/^\/board(?:\/([^/]+))?$/);
20644
21321
  if (boardMatch) {
20645
21322
  const type = boardMatch[1];
@@ -22135,6 +22812,24 @@ function createWebTools(store, projectName, navGroups) {
22135
22812
  };
22136
22813
  },
22137
22814
  { annotations: { readOnlyHint: true } }
22815
+ ),
22816
+ tool22(
22817
+ "get_dashboard_sprint_summary",
22818
+ "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.",
22819
+ {
22820
+ sprint: external_exports.string().optional().describe("Sprint ID (e.g. 'SP-001'). Omit for the active sprint.")
22821
+ },
22822
+ async (args) => {
22823
+ const data = getSprintSummaryData(store, args.sprint);
22824
+ if (!data) {
22825
+ const msg = args.sprint ? `Sprint ${args.sprint} not found.` : "No active sprint found.";
22826
+ return { content: [{ type: "text", text: msg }], isError: true };
22827
+ }
22828
+ return {
22829
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
22830
+ };
22831
+ },
22832
+ { annotations: { readOnlyHint: true } }
22138
22833
  )
22139
22834
  ];
22140
22835
  }
@@ -22160,11 +22855,11 @@ function createMarvinMcpServer(store, options) {
22160
22855
  }
22161
22856
 
22162
22857
  // src/agent/session-namer.ts
22163
- import { query } from "@anthropic-ai/claude-agent-sdk";
22858
+ import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
22164
22859
  async function generateSessionName(turns) {
22165
22860
  try {
22166
22861
  const transcript = turns.slice(-20).map((t) => `${t.role}: ${t.content.slice(0, 200)}`).join("\n");
22167
- const result = query({
22862
+ const result = query2({
22168
22863
  prompt: `Summarize this conversation in 3-5 words as a kebab-case name suitable for a filename. Output ONLY the name, nothing else.
22169
22864
 
22170
22865
  ${transcript}`,
@@ -22431,6 +23126,7 @@ Marvin \u2014 ${persona.name}
22431
23126
  "mcp__marvin-governance__get_dashboard_gar",
22432
23127
  "mcp__marvin-governance__get_dashboard_board",
22433
23128
  "mcp__marvin-governance__get_dashboard_upcoming",
23129
+ "mcp__marvin-governance__get_dashboard_sprint_summary",
22434
23130
  ...pluginTools.map((t) => `mcp__marvin-governance__${t.name}`),
22435
23131
  ...codeSkillTools.map((t) => `mcp__marvin-governance__${t.name}`)
22436
23132
  ]
@@ -22441,7 +23137,7 @@ Marvin \u2014 ${persona.name}
22441
23137
  if (existingSession) {
22442
23138
  queryOptions.resume = existingSession.id;
22443
23139
  }
22444
- const conversation = query2({
23140
+ const conversation = query3({
22445
23141
  prompt,
22446
23142
  options: queryOptions
22447
23143
  });
@@ -22800,7 +23496,7 @@ import * as fs12 from "fs";
22800
23496
  import * as path12 from "path";
22801
23497
  import chalk7 from "chalk";
22802
23498
  import ora2 from "ora";
22803
- import { query as query3 } from "@anthropic-ai/claude-agent-sdk";
23499
+ import { query as query4 } from "@anthropic-ai/claude-agent-sdk";
22804
23500
 
22805
23501
  // src/sources/prompts.ts
22806
23502
  function buildIngestSystemPrompt(persona, projectConfig, isDraft) {
@@ -22933,7 +23629,7 @@ async function ingestFile(options) {
22933
23629
  const spinner = ora2({ text: `Analyzing ${fileName}...`, color: "cyan" });
22934
23630
  spinner.start();
22935
23631
  try {
22936
- const conversation = query3({
23632
+ const conversation = query4({
22937
23633
  prompt: userPrompt,
22938
23634
  options: {
22939
23635
  systemPrompt,
@@ -23463,7 +24159,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
23463
24159
  import { tool as tool23 } from "@anthropic-ai/claude-agent-sdk";
23464
24160
 
23465
24161
  // src/skills/action-runner.ts
23466
- import { query as query4 } from "@anthropic-ai/claude-agent-sdk";
24162
+ import { query as query5 } from "@anthropic-ai/claude-agent-sdk";
23467
24163
  var GOVERNANCE_TOOL_NAMES2 = [
23468
24164
  "mcp__marvin-governance__list_decisions",
23469
24165
  "mcp__marvin-governance__get_decision",
@@ -23485,7 +24181,7 @@ async function runSkillAction(action, userPrompt, context) {
23485
24181
  try {
23486
24182
  const mcpServer = createMarvinMcpServer(context.store);
23487
24183
  const allowedTools = action.allowGovernanceTools !== false ? GOVERNANCE_TOOL_NAMES2 : [];
23488
- const conversation = query4({
24184
+ const conversation = query5({
23489
24185
  prompt: userPrompt,
23490
24186
  options: {
23491
24187
  systemPrompt: action.systemPrompt,
@@ -24526,7 +25222,7 @@ import chalk13 from "chalk";
24526
25222
  // src/analysis/analyze.ts
24527
25223
  import chalk12 from "chalk";
24528
25224
  import ora4 from "ora";
24529
- import { query as query5 } from "@anthropic-ai/claude-agent-sdk";
25225
+ import { query as query6 } from "@anthropic-ai/claude-agent-sdk";
24530
25226
 
24531
25227
  // src/analysis/prompts.ts
24532
25228
  function buildAnalyzeSystemPrompt(persona, projectConfig, isDraft) {
@@ -24656,7 +25352,7 @@ async function analyzeMeeting(options) {
24656
25352
  const spinner = ora4({ text: `Analyzing meeting ${meetingId}...`, color: "cyan" });
24657
25353
  spinner.start();
24658
25354
  try {
24659
- const conversation = query5({
25355
+ const conversation = query6({
24660
25356
  prompt: userPrompt,
24661
25357
  options: {
24662
25358
  systemPrompt,
@@ -24783,7 +25479,7 @@ import chalk15 from "chalk";
24783
25479
  // src/contributions/contribute.ts
24784
25480
  import chalk14 from "chalk";
24785
25481
  import ora5 from "ora";
24786
- import { query as query6 } from "@anthropic-ai/claude-agent-sdk";
25482
+ import { query as query7 } from "@anthropic-ai/claude-agent-sdk";
24787
25483
 
24788
25484
  // src/contributions/prompts.ts
24789
25485
  function buildContributeSystemPrompt(persona, contributionType, projectConfig, isDraft) {
@@ -25037,7 +25733,7 @@ async function contributeFromPersona(options) {
25037
25733
  "mcp__marvin-governance__get_action",
25038
25734
  "mcp__marvin-governance__get_question"
25039
25735
  ];
25040
- const conversation = query6({
25736
+ const conversation = query7({
25041
25737
  prompt: userPrompt,
25042
25738
  options: {
25043
25739
  systemPrompt,
@@ -25183,6 +25879,9 @@ Contribution: ${options.type}`));
25183
25879
  });
25184
25880
  }
25185
25881
 
25882
+ // src/cli/commands/report.ts
25883
+ import ora6 from "ora";
25884
+
25186
25885
  // src/reports/gar/render-ascii.ts
25187
25886
  import chalk16 from "chalk";
25188
25887
  var STATUS_DOT = {
@@ -25377,6 +26076,47 @@ async function healthReportCommand(options) {
25377
26076
  console.log(renderAscii2(report));
25378
26077
  }
25379
26078
  }
26079
+ async function sprintSummaryCommand(options) {
26080
+ const project = loadProject();
26081
+ const plugin = resolvePlugin(project.config.methodology);
26082
+ const pluginRegistrations = plugin?.documentTypeRegistrations ?? [];
26083
+ const allSkills = loadAllSkills(project.marvinDir);
26084
+ const allSkillIds = [...allSkills.keys()];
26085
+ const skillRegistrations = collectSkillRegistrations(allSkillIds, allSkills);
26086
+ const store = new DocumentStore(project.marvinDir, [...pluginRegistrations, ...skillRegistrations]);
26087
+ const data = collectSprintSummaryData(store, options.sprint);
26088
+ if (!data) {
26089
+ const msg = options.sprint ? `Sprint ${options.sprint} not found.` : "No active sprint found. Use --sprint <id> to specify one.";
26090
+ console.error(msg);
26091
+ process.exit(1);
26092
+ }
26093
+ const spinner = ora6({ text: "Generating AI sprint summary...", color: "cyan" }).start();
26094
+ try {
26095
+ const summary = await generateSprintSummary(data);
26096
+ spinner.stop();
26097
+ const header = `# Sprint Summary: ${data.sprint.id} \u2014 ${data.sprint.title}
26098
+
26099
+ `;
26100
+ console.log(header + summary);
26101
+ if (options.save) {
26102
+ const doc = store.create(
26103
+ "report",
26104
+ {
26105
+ title: `Sprint Summary: ${data.sprint.title}`,
26106
+ status: "final",
26107
+ tags: [`report-type:sprint-summary`, `sprint:${data.sprint.id}`]
26108
+ },
26109
+ summary
26110
+ );
26111
+ console.log(`
26112
+ Saved as ${doc.frontmatter.id}`);
26113
+ }
26114
+ } catch (err) {
26115
+ spinner.stop();
26116
+ console.error("Failed to generate sprint summary:", err);
26117
+ process.exit(1);
26118
+ }
26119
+ }
25380
26120
 
25381
26121
  // src/cli/commands/web.ts
25382
26122
  async function webCommand(options) {
@@ -25419,7 +26159,7 @@ function createProgram() {
25419
26159
  const program2 = new Command();
25420
26160
  program2.name("marvin").description(
25421
26161
  "AI-powered product development assistant with Product Owner, Delivery Manager, and Technical Lead personas"
25422
- ).version("0.4.6");
26162
+ ).version("0.4.7");
25423
26163
  program2.command("init").description("Initialize a new Marvin project in the current directory").action(async () => {
25424
26164
  await initCommand();
25425
26165
  });
@@ -25502,6 +26242,9 @@ function createProgram() {
25502
26242
  ).action(async (options) => {
25503
26243
  await healthReportCommand(options);
25504
26244
  });
26245
+ 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) => {
26246
+ await sprintSummaryCommand(options);
26247
+ });
25505
26248
  program2.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) => {
25506
26249
  await webCommand(options);
25507
26250
  });