mrvn-cli 0.4.5 → 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,8 +18891,28 @@ 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
18899
+ function collapsibleSection(sectionId, title, content, opts) {
18900
+ const tag = opts?.titleTag ?? "div";
18901
+ const cls = opts?.titleClass ?? "section-title";
18902
+ const collapsed = opts?.defaultCollapsed ? " collapsed" : "";
18903
+ return `
18904
+ <div class="collapsible${collapsed}" data-section-id="${escapeHtml(sectionId)}">
18905
+ <${tag} class="${cls} collapsible-header" onclick="toggleSection(this)">
18906
+ <svg class="collapsible-chevron" viewBox="0 0 16 16" width="14" height="14" fill="currentColor">
18907
+ <path d="M4.94 5.72a.75.75 0 0 1 1.06-.02L8 7.56l1.97-1.84a.75.75 0 1 1 1.02 1.1l-2.5 2.34a.75.75 0 0 1-1.02 0l-2.5-2.34a.75.75 0 0 1-.03-1.06z"/>
18908
+ </svg>
18909
+ <span>${title}</span>
18910
+ </${tag}>
18911
+ <div class="collapsible-body">
18912
+ ${content}
18913
+ </div>
18914
+ </div>`;
18915
+ }
18557
18916
  function escapeHtml(str) {
18558
18917
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
18559
18918
  }
@@ -18676,6 +19035,7 @@ function layout(opts, body) {
18676
19035
  const topItems = [
18677
19036
  { href: "/", label: "Overview" },
18678
19037
  { href: "/upcoming", label: "Upcoming" },
19038
+ { href: "/sprint-summary", label: "Sprint Summary" },
18679
19039
  { href: "/timeline", label: "Timeline" },
18680
19040
  { href: "/board", label: "Board" },
18681
19041
  { href: "/gar", label: "GAR Report" },
@@ -18721,6 +19081,32 @@ function layout(opts, body) {
18721
19081
  ${body}
18722
19082
  </main>
18723
19083
  </div>
19084
+ <script>
19085
+ function toggleSection(header) {
19086
+ var section = header.closest('.collapsible');
19087
+ if (!section) return;
19088
+ section.classList.toggle('collapsed');
19089
+ var id = section.getAttribute('data-section-id');
19090
+ if (id) {
19091
+ try {
19092
+ var state = JSON.parse(localStorage.getItem('marvin-collapsed') || '{}');
19093
+ state[id] = section.classList.contains('collapsed');
19094
+ localStorage.setItem('marvin-collapsed', JSON.stringify(state));
19095
+ } catch(e) {}
19096
+ }
19097
+ }
19098
+ // Restore collapsed state on load
19099
+ (function() {
19100
+ try {
19101
+ var state = JSON.parse(localStorage.getItem('marvin-collapsed') || '{}');
19102
+ document.querySelectorAll('.collapsible[data-section-id]').forEach(function(el) {
19103
+ var id = el.getAttribute('data-section-id');
19104
+ if (state[id] === true) el.classList.add('collapsed');
19105
+ else if (state[id] === false) el.classList.remove('collapsed');
19106
+ });
19107
+ } catch(e) {}
19108
+ })();
19109
+ </script>
18724
19110
  <script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
18725
19111
  <script>mermaid.initialize({
18726
19112
  startOnLoad: true,
@@ -19544,13 +19930,60 @@ tr:hover td {
19544
19930
  white-space: nowrap;
19545
19931
  }
19546
19932
 
19933
+ .gantt-grid-line {
19934
+ position: absolute;
19935
+ top: 0;
19936
+ bottom: 0;
19937
+ width: 1px;
19938
+ background: var(--border);
19939
+ opacity: 0.35;
19940
+ }
19941
+
19942
+ .gantt-sprint-line {
19943
+ position: absolute;
19944
+ top: 0;
19945
+ bottom: 0;
19946
+ width: 1px;
19947
+ background: var(--text-dim);
19948
+ opacity: 0.3;
19949
+ }
19950
+
19547
19951
  .gantt-today {
19548
19952
  position: absolute;
19549
19953
  top: 0;
19550
19954
  bottom: 0;
19551
- width: 2px;
19955
+ width: 3px;
19552
19956
  background: var(--red);
19553
- opacity: 0.7;
19957
+ opacity: 0.8;
19958
+ border-radius: 1px;
19959
+ }
19960
+
19961
+ /* Sprint band in timeline */
19962
+ .gantt-sprint-band-row {
19963
+ border-bottom: 1px solid var(--border);
19964
+ margin-bottom: 0.25rem;
19965
+ }
19966
+
19967
+ .gantt-sprint-band {
19968
+ height: 32px;
19969
+ }
19970
+
19971
+ .gantt-sprint-block {
19972
+ position: absolute;
19973
+ top: 2px;
19974
+ bottom: 2px;
19975
+ background: var(--bg-hover);
19976
+ border: 1px solid var(--border);
19977
+ border-radius: 4px;
19978
+ font-size: 0.65rem;
19979
+ color: var(--text-dim);
19980
+ display: flex;
19981
+ align-items: center;
19982
+ justify-content: center;
19983
+ overflow: hidden;
19984
+ white-space: nowrap;
19985
+ text-overflow: ellipsis;
19986
+ padding: 0 0.4rem;
19554
19987
  }
19555
19988
 
19556
19989
  /* Pie chart color overrides */
@@ -19612,6 +20045,146 @@ tr:hover td {
19612
20045
  }
19613
20046
 
19614
20047
  .text-dim { color: var(--text-dim); }
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
+
20155
+ /* Collapsible sections */
20156
+ .collapsible-header {
20157
+ cursor: pointer;
20158
+ display: flex;
20159
+ align-items: center;
20160
+ gap: 0.4rem;
20161
+ user-select: none;
20162
+ }
20163
+
20164
+ .collapsible-header:hover {
20165
+ color: var(--accent);
20166
+ }
20167
+
20168
+ .collapsible-chevron {
20169
+ transition: transform 0.2s ease;
20170
+ flex-shrink: 0;
20171
+ }
20172
+
20173
+ .collapsible.collapsed .collapsible-chevron {
20174
+ transform: rotate(-90deg);
20175
+ }
20176
+
20177
+ .collapsible-body {
20178
+ overflow: hidden;
20179
+ max-height: 5000px;
20180
+ transition: max-height 0.3s ease, opacity 0.2s ease;
20181
+ opacity: 1;
20182
+ }
20183
+
20184
+ .collapsible.collapsed .collapsible-body {
20185
+ max-height: 0;
20186
+ opacity: 0;
20187
+ }
19615
20188
  `;
19616
20189
  }
19617
20190
 
@@ -19664,35 +20237,73 @@ function buildTimelineGantt(data, maxSprints = 6) {
19664
20237
  );
19665
20238
  tick += 7 * DAY;
19666
20239
  }
20240
+ const gridLines = [];
20241
+ let gridTick = timelineStart;
20242
+ const gridStartDay = new Date(gridTick).getDay();
20243
+ gridTick += (8 - gridStartDay) % 7 * DAY;
20244
+ while (gridTick <= timelineEnd) {
20245
+ gridLines.push(`<div class="gantt-grid-line" style="left:${pct(gridTick).toFixed(2)}%"></div>`);
20246
+ gridTick += 7 * DAY;
20247
+ }
20248
+ const sprintBoundaries = /* @__PURE__ */ new Set();
20249
+ for (const sprint of visibleSprints) {
20250
+ sprintBoundaries.add(toMs(sprint.startDate));
20251
+ sprintBoundaries.add(toMs(sprint.endDate));
20252
+ }
20253
+ const sprintLines = [...sprintBoundaries].map(
20254
+ (ms) => `<div class="gantt-sprint-line" style="left:${pct(ms).toFixed(2)}%"></div>`
20255
+ );
19667
20256
  const now = Date.now();
19668
20257
  let todayMarker = "";
19669
20258
  if (now >= timelineStart && now <= timelineEnd) {
19670
20259
  todayMarker = `<div class="gantt-today" style="left:${pct(now).toFixed(2)}%"></div>`;
19671
20260
  }
19672
- const rows = [];
20261
+ const sprintBlocks = visibleSprints.map((sprint) => {
20262
+ const sStart = toMs(sprint.startDate);
20263
+ const sEnd = toMs(sprint.endDate);
20264
+ const left = pct(sStart).toFixed(2);
20265
+ const width = (pct(sEnd) - pct(sStart)).toFixed(2);
20266
+ return `<div class="gantt-sprint-block" style="left:${left}%;width:${width}%">${sanitize(sprint.id, 20)}</div>`;
20267
+ }).join("");
20268
+ const sprintBandRow = `<div class="gantt-row gantt-sprint-band-row">
20269
+ <div class="gantt-label gantt-section-label">Sprints</div>
20270
+ <div class="gantt-track gantt-sprint-band">${sprintBlocks}</div>
20271
+ </div>`;
20272
+ const epicSpanMap = /* @__PURE__ */ new Map();
19673
20273
  for (const sprint of visibleSprints) {
19674
20274
  const sStart = toMs(sprint.startDate);
19675
20275
  const sEnd = toMs(sprint.endDate);
19676
- rows.push(`<div class="gantt-section-row">
19677
- <div class="gantt-label gantt-section-label">${sanitize(sprint.id + " " + sprint.title, 50)}</div>
19678
- <div class="gantt-track">
19679
- <div class="gantt-section-bg" style="left:${pct(sStart).toFixed(2)}%;width:${(pct(sEnd) - pct(sStart)).toFixed(2)}%"></div>
19680
- </div>
19681
- </div>`);
19682
- const linked = sprint.linkedEpics.map((eid) => epicMap.get(eid)).filter(Boolean);
19683
- const items = linked.length > 0 ? linked.map((e) => ({ label: sanitize(e.id + " " + e.title), status: e.status })) : [{ label: sanitize(sprint.title), status: sprint.status }];
19684
- for (const item of items) {
19685
- const cls = item.status === "done" || item.status === "completed" ? "gantt-bar-done" : item.status === "in-progress" || item.status === "active" ? "gantt-bar-active" : item.status === "blocked" ? "gantt-bar-blocked" : "gantt-bar-default";
19686
- const left = pct(sStart).toFixed(2);
19687
- const width = (pct(sEnd) - pct(sStart)).toFixed(2);
19688
- rows.push(`<div class="gantt-row">
19689
- <div class="gantt-label">${item.label}</div>
20276
+ for (const eid of sprint.linkedEpics) {
20277
+ if (!epicMap.has(eid)) continue;
20278
+ const existing = epicSpanMap.get(eid);
20279
+ if (existing) {
20280
+ existing.startMs = Math.min(existing.startMs, sStart);
20281
+ existing.endMs = Math.max(existing.endMs, sEnd);
20282
+ } else {
20283
+ epicSpanMap.set(eid, { startMs: sStart, endMs: sEnd });
20284
+ }
20285
+ }
20286
+ }
20287
+ const sortedEpicIds = [...epicSpanMap.keys()].sort((a, b) => {
20288
+ const aSpan = epicSpanMap.get(a);
20289
+ const bSpan = epicSpanMap.get(b);
20290
+ if (aSpan.startMs !== bSpan.startMs) return aSpan.startMs - bSpan.startMs;
20291
+ return a.localeCompare(b);
20292
+ });
20293
+ const epicRows = sortedEpicIds.map((eid) => {
20294
+ const epic = epicMap.get(eid);
20295
+ const { startMs, endMs } = epicSpanMap.get(eid);
20296
+ const cls = epic.status === "done" || epic.status === "completed" ? "gantt-bar-done" : epic.status === "in-progress" || epic.status === "active" ? "gantt-bar-active" : epic.status === "blocked" ? "gantt-bar-blocked" : "gantt-bar-default";
20297
+ const left = pct(startMs).toFixed(2);
20298
+ const width = (pct(endMs) - pct(startMs)).toFixed(2);
20299
+ const label = sanitize(epic.id + " " + epic.title);
20300
+ return `<div class="gantt-row">
20301
+ <div class="gantt-label">${label}</div>
19690
20302
  <div class="gantt-track">
19691
20303
  <div class="gantt-bar ${cls}" style="left:${left}%;width:${width}%"></div>
19692
20304
  </div>
19693
- </div>`);
19694
- }
19695
- }
20305
+ </div>`;
20306
+ }).join("\n");
19696
20307
  const note = truncated ? `<div class="mermaid-note">${hiddenCount} earlier sprint${hiddenCount > 1 ? "s" : ""} not shown</div>` : "";
19697
20308
  return `${note}
19698
20309
  <div class="gantt">
@@ -19701,11 +20312,12 @@ function buildTimelineGantt(data, maxSprints = 6) {
19701
20312
  <div class="gantt-label"></div>
19702
20313
  <div class="gantt-track gantt-dates">${markers.join("")}</div>
19703
20314
  </div>
19704
- ${rows.join("\n")}
20315
+ ${sprintBandRow}
20316
+ ${epicRows}
19705
20317
  </div>
19706
20318
  <div class="gantt-overlay">
19707
20319
  <div class="gantt-label"></div>
19708
- <div class="gantt-track">${todayMarker}</div>
20320
+ <div class="gantt-track">${gridLines.join("")}${sprintLines.join("")}${todayMarker}</div>
19709
20321
  </div>
19710
20322
  </div>`;
19711
20323
  }
@@ -19985,11 +20597,12 @@ function overviewPage(data, diagrams, navGroups) {
19985
20597
 
19986
20598
  <div class="section-title"><a href="/timeline">Project Timeline &rarr;</a></div>
19987
20599
 
19988
- <div class="section-title">Artifact Relationships</div>
19989
- ${buildArtifactFlowchart(diagrams)}
20600
+ ${collapsibleSection("overview-relationships", "Artifact Relationships", buildArtifactFlowchart(diagrams))}
19990
20601
 
19991
- <div class="section-title">Recent Activity</div>
19992
- ${data.recent.length > 0 ? `
20602
+ ${collapsibleSection(
20603
+ "overview-recent",
20604
+ "Recent Activity",
20605
+ data.recent.length > 0 ? `
19993
20606
  <div class="table-wrap">
19994
20607
  <table>
19995
20608
  <thead>
@@ -20005,7 +20618,8 @@ function overviewPage(data, diagrams, navGroups) {
20005
20618
  ${rows}
20006
20619
  </tbody>
20007
20620
  </table>
20008
- </div>` : `<div class="empty"><p>No documents yet.</p></div>`}
20621
+ </div>` : `<div class="empty"><p>No documents yet.</p></div>`
20622
+ )}
20009
20623
  `;
20010
20624
  }
20011
20625
 
@@ -20150,23 +20764,24 @@ function garPage(report) {
20150
20764
  <div class="label">Overall: ${escapeHtml(report.overall)}</div>
20151
20765
  </div>
20152
20766
 
20153
- <div class="gar-areas">
20154
- ${areaCards}
20155
- </div>
20767
+ ${collapsibleSection("gar-areas", "Areas", `<div class="gar-areas">${areaCards}</div>`)}
20156
20768
 
20157
- <div class="section-title">Status Distribution</div>
20158
- ${buildStatusPie("Action Status", {
20159
- Open: report.metrics.scope.open,
20160
- Done: report.metrics.scope.done,
20161
- "In Progress": Math.max(0, report.metrics.scope.total - report.metrics.scope.open - report.metrics.scope.done)
20162
- })}
20769
+ ${collapsibleSection(
20770
+ "gar-status-dist",
20771
+ "Status Distribution",
20772
+ buildStatusPie("Action Status", {
20773
+ Open: report.metrics.scope.open,
20774
+ Done: report.metrics.scope.done,
20775
+ "In Progress": Math.max(0, report.metrics.scope.total - report.metrics.scope.open - report.metrics.scope.done)
20776
+ })
20777
+ )}
20163
20778
  `;
20164
20779
  }
20165
20780
 
20166
20781
  // src/web/templates/pages/health.ts
20167
20782
  function healthPage(report, metrics) {
20168
20783
  const dotClass = `dot-${report.overall}`;
20169
- function renderSection(title, categories) {
20784
+ function renderSection(sectionId, title, categories) {
20170
20785
  const cards = categories.map(
20171
20786
  (cat) => `
20172
20787
  <div class="gar-area">
@@ -20178,10 +20793,9 @@ function healthPage(report, metrics) {
20178
20793
  ${cat.items.length > 0 ? `<ul>${cat.items.map((item) => `<li><span class="ref-id">${escapeHtml(item.id)}</span>${escapeHtml(item.detail)}</li>`).join("")}</ul>` : ""}
20179
20794
  </div>`
20180
20795
  ).join("\n");
20181
- return `
20182
- <div class="health-section-title">${escapeHtml(title)}</div>
20183
- <div class="gar-areas">${cards}</div>
20184
- `;
20796
+ return collapsibleSection(sectionId, title, `<div class="gar-areas">${cards}</div>`, {
20797
+ titleClass: "health-section-title"
20798
+ });
20185
20799
  }
20186
20800
  return `
20187
20801
  <div class="page-header">
@@ -20194,35 +20808,43 @@ function healthPage(report, metrics) {
20194
20808
  <div class="label">Overall: ${escapeHtml(report.overall)}</div>
20195
20809
  </div>
20196
20810
 
20197
- ${renderSection("Completeness", report.completeness)}
20198
-
20199
- <div class="health-section-title">Completeness Overview</div>
20200
- ${buildHealthGauge(
20201
- metrics ? Object.entries(metrics.completeness).map(([name, cat]) => ({
20202
- name: name.replace(/\b\w/g, (c) => c.toUpperCase()),
20203
- complete: cat.complete,
20204
- total: cat.total
20205
- })) : report.completeness.map((c) => {
20206
- const match = c.summary.match(/(\d+)\s*\/\s*(\d+)/);
20207
- return {
20208
- name: c.name,
20209
- complete: match ? parseInt(match[1], 10) : 0,
20210
- total: match ? parseInt(match[2], 10) : 0
20211
- };
20212
- })
20811
+ ${renderSection("health-completeness", "Completeness", report.completeness)}
20812
+
20813
+ ${collapsibleSection(
20814
+ "health-completeness-overview",
20815
+ "Completeness Overview",
20816
+ buildHealthGauge(
20817
+ metrics ? Object.entries(metrics.completeness).map(([name, cat]) => ({
20818
+ name: name.replace(/\b\w/g, (c) => c.toUpperCase()),
20819
+ complete: cat.complete,
20820
+ total: cat.total
20821
+ })) : report.completeness.map((c) => {
20822
+ const match = c.summary.match(/(\d+)\s*\/\s*(\d+)/);
20823
+ return {
20824
+ name: c.name,
20825
+ complete: match ? parseInt(match[1], 10) : 0,
20826
+ total: match ? parseInt(match[2], 10) : 0
20827
+ };
20828
+ })
20829
+ ),
20830
+ { titleClass: "health-section-title" }
20213
20831
  )}
20214
20832
 
20215
- ${renderSection("Process", report.process)}
20216
-
20217
- <div class="health-section-title">Process Summary</div>
20218
- ${metrics ? buildStatusPie("Process Health", {
20219
- Stale: metrics.process.stale.length,
20220
- "Aging Actions": metrics.process.agingActions.length,
20221
- Healthy: Math.max(
20222
- 0,
20223
- (metrics.completeness ? Object.values(metrics.completeness).reduce((sum, c) => sum + c.total, 0) : 0) - metrics.process.stale.length - metrics.process.agingActions.length
20224
- )
20225
- }) : ""}
20833
+ ${renderSection("health-process", "Process", report.process)}
20834
+
20835
+ ${collapsibleSection(
20836
+ "health-process-summary",
20837
+ "Process Summary",
20838
+ metrics ? buildStatusPie("Process Health", {
20839
+ Stale: metrics.process.stale.length,
20840
+ "Aging Actions": metrics.process.agingActions.length,
20841
+ Healthy: Math.max(
20842
+ 0,
20843
+ (metrics.completeness ? Object.values(metrics.completeness).reduce((sum, c) => sum + c.total, 0) : 0) - metrics.process.stale.length - metrics.process.agingActions.length
20844
+ )
20845
+ }) : "",
20846
+ { titleClass: "health-section-title" }
20847
+ )}
20226
20848
  `;
20227
20849
  }
20228
20850
 
@@ -20280,7 +20902,7 @@ function timelinePage(diagrams) {
20280
20902
  return `
20281
20903
  <div class="page-header">
20282
20904
  <h2>Project Timeline</h2>
20283
- <div class="subtitle">Sprint schedule with linked epics</div>
20905
+ <div class="subtitle">Epic timeline across sprints</div>
20284
20906
  </div>
20285
20907
 
20286
20908
  ${buildTimelineGantt(diagrams)}
@@ -20308,9 +20930,10 @@ function upcomingPage(data) {
20308
20930
  const hasActions = data.dueSoonActions.length > 0;
20309
20931
  const hasSprintTasks = data.dueSoonSprintTasks.length > 0;
20310
20932
  const hasTrending = data.trending.length > 0;
20311
- const actionsTable = hasActions ? `
20312
- <h3 class="section-title">Due Soon \u2014 Actions</h3>
20313
- <div class="table-wrap">
20933
+ const actionsTable = hasActions ? collapsibleSection(
20934
+ "upcoming-actions",
20935
+ "Due Soon \u2014 Actions",
20936
+ `<div class="table-wrap">
20314
20937
  <table>
20315
20938
  <thead>
20316
20939
  <tr>
@@ -20325,7 +20948,7 @@ function upcomingPage(data) {
20325
20948
  </thead>
20326
20949
  <tbody>
20327
20950
  ${data.dueSoonActions.map(
20328
- (a) => `
20951
+ (a) => `
20329
20952
  <tr class="${urgencyRowClass(a.urgency)}">
20330
20953
  <td><a href="/docs/action/${escapeHtml(a.id)}">${escapeHtml(a.id)}</a></td>
20331
20954
  <td>${escapeHtml(a.title)}</td>
@@ -20335,13 +20958,16 @@ function upcomingPage(data) {
20335
20958
  <td>${urgencyBadge(a.urgency)}</td>
20336
20959
  <td>${a.relatedTaskCount > 0 ? a.relatedTaskCount : "\u2014"}</td>
20337
20960
  </tr>`
20338
- ).join("")}
20961
+ ).join("")}
20339
20962
  </tbody>
20340
20963
  </table>
20341
- </div>` : "";
20342
- const sprintTasksTable = hasSprintTasks ? `
20343
- <h3 class="section-title">Due Soon \u2014 Sprint Tasks</h3>
20344
- <div class="table-wrap">
20964
+ </div>`,
20965
+ { titleTag: "h3" }
20966
+ ) : "";
20967
+ const sprintTasksTable = hasSprintTasks ? collapsibleSection(
20968
+ "upcoming-sprint-tasks",
20969
+ "Due Soon \u2014 Sprint Tasks",
20970
+ `<div class="table-wrap">
20345
20971
  <table>
20346
20972
  <thead>
20347
20973
  <tr>
@@ -20355,7 +20981,7 @@ function upcomingPage(data) {
20355
20981
  </thead>
20356
20982
  <tbody>
20357
20983
  ${data.dueSoonSprintTasks.map(
20358
- (t) => `
20984
+ (t) => `
20359
20985
  <tr class="${urgencyRowClass(t.urgency)}">
20360
20986
  <td><a href="/docs/task/${escapeHtml(t.id)}">${escapeHtml(t.id)}</a></td>
20361
20987
  <td>${escapeHtml(t.title)}</td>
@@ -20364,13 +20990,16 @@ function upcomingPage(data) {
20364
20990
  <td>${formatDate(t.sprintEndDate)}</td>
20365
20991
  <td>${urgencyBadge(t.urgency)}</td>
20366
20992
  </tr>`
20367
- ).join("")}
20993
+ ).join("")}
20368
20994
  </tbody>
20369
20995
  </table>
20370
- </div>` : "";
20371
- const trendingTable = hasTrending ? `
20372
- <h3 class="section-title">Trending</h3>
20373
- <div class="table-wrap">
20996
+ </div>`,
20997
+ { titleTag: "h3" }
20998
+ ) : "";
20999
+ const trendingTable = hasTrending ? collapsibleSection(
21000
+ "upcoming-trending",
21001
+ "Trending",
21002
+ `<div class="table-wrap">
20374
21003
  <table>
20375
21004
  <thead>
20376
21005
  <tr>
@@ -20385,7 +21014,7 @@ function upcomingPage(data) {
20385
21014
  </thead>
20386
21015
  <tbody>
20387
21016
  ${data.trending.map(
20388
- (t, i) => `
21017
+ (t, i) => `
20389
21018
  <tr>
20390
21019
  <td><span class="trending-rank">${i + 1}</span></td>
20391
21020
  <td><a href="/docs/${escapeHtml(t.type)}/${escapeHtml(t.id)}">${escapeHtml(t.id)}</a></td>
@@ -20395,10 +21024,12 @@ function upcomingPage(data) {
20395
21024
  <td><span class="trending-score">${t.score}</span></td>
20396
21025
  <td>${t.signals.map((s) => `<span class="signal-tag">${escapeHtml(s.factor)} +${s.points}</span>`).join(" ")}</td>
20397
21026
  </tr>`
20398
- ).join("")}
21027
+ ).join("")}
20399
21028
  </tbody>
20400
21029
  </table>
20401
- </div>` : "";
21030
+ </div>`,
21031
+ { titleTag: "h3" }
21032
+ ) : "";
20402
21033
  const emptyState = !hasActions && !hasSprintTasks && !hasTrending ? '<div class="empty"><p>No upcoming items or trending activity found.</p></div>' : "";
20403
21034
  return `
20404
21035
  <div class="page-header">
@@ -20412,7 +21043,199 @@ function upcomingPage(data) {
20412
21043
  `;
20413
21044
  }
20414
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
+
20415
21237
  // src/web/router.ts
21238
+ var sprintSummaryCache = /* @__PURE__ */ new Map();
20416
21239
  function handleRequest(req, res, store, projectName, navGroups) {
20417
21240
  const parsed = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
20418
21241
  const pathname = parsed.pathname;
@@ -20458,6 +21281,42 @@ function handleRequest(req, res, store, projectName, navGroups) {
20458
21281
  respond(res, layout({ title: "Upcoming", activePath: "/upcoming", projectName, navGroups }, body));
20459
21282
  return;
20460
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
+ }
20461
21320
  const boardMatch = pathname.match(/^\/board(?:\/([^/]+))?$/);
20462
21321
  if (boardMatch) {
20463
21322
  const type = boardMatch[1];
@@ -21115,8 +21974,8 @@ function gatherContext(store, focusFeature, includeDecisions = true, includeQues
21115
21974
  title: e.frontmatter.title,
21116
21975
  status: e.frontmatter.status,
21117
21976
  linkedFeature: normalizeLinkedFeatures(e.frontmatter.linkedFeature),
21118
- targetDate: e.frontmatter.targetDate ?? null,
21119
- estimatedEffort: e.frontmatter.estimatedEffort ?? null,
21977
+ targetDate: typeof e.frontmatter.targetDate === "string" ? e.frontmatter.targetDate : null,
21978
+ estimatedEffort: typeof e.frontmatter.estimatedEffort === "string" ? e.frontmatter.estimatedEffort : null,
21120
21979
  content: e.content,
21121
21980
  linkedTaskCount: tasks.filter(
21122
21981
  (t) => normalizeLinkedEpics(t.frontmatter.linkedEpic).includes(e.frontmatter.id)
@@ -21127,10 +21986,10 @@ function gatherContext(store, focusFeature, includeDecisions = true, includeQues
21127
21986
  title: t.frontmatter.title,
21128
21987
  status: t.frontmatter.status,
21129
21988
  linkedEpic: normalizeLinkedEpics(t.frontmatter.linkedEpic),
21130
- acceptanceCriteria: t.frontmatter.acceptanceCriteria ?? null,
21131
- technicalNotes: t.frontmatter.technicalNotes ?? null,
21132
- complexity: t.frontmatter.complexity ?? null,
21133
- estimatedPoints: t.frontmatter.estimatedPoints ?? null,
21989
+ acceptanceCriteria: typeof t.frontmatter.acceptanceCriteria === "string" ? t.frontmatter.acceptanceCriteria : null,
21990
+ technicalNotes: typeof t.frontmatter.technicalNotes === "string" ? t.frontmatter.technicalNotes : null,
21991
+ complexity: typeof t.frontmatter.complexity === "string" ? t.frontmatter.complexity : null,
21992
+ estimatedPoints: typeof t.frontmatter.estimatedPoints === "number" ? t.frontmatter.estimatedPoints : null,
21134
21993
  priority: t.frontmatter.priority ?? null
21135
21994
  })),
21136
21995
  decisions: allDecisions.map((d) => ({
@@ -21953,6 +22812,24 @@ function createWebTools(store, projectName, navGroups) {
21953
22812
  };
21954
22813
  },
21955
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 } }
21956
22833
  )
21957
22834
  ];
21958
22835
  }
@@ -21978,11 +22855,11 @@ function createMarvinMcpServer(store, options) {
21978
22855
  }
21979
22856
 
21980
22857
  // src/agent/session-namer.ts
21981
- import { query } from "@anthropic-ai/claude-agent-sdk";
22858
+ import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
21982
22859
  async function generateSessionName(turns) {
21983
22860
  try {
21984
22861
  const transcript = turns.slice(-20).map((t) => `${t.role}: ${t.content.slice(0, 200)}`).join("\n");
21985
- const result = query({
22862
+ const result = query2({
21986
22863
  prompt: `Summarize this conversation in 3-5 words as a kebab-case name suitable for a filename. Output ONLY the name, nothing else.
21987
22864
 
21988
22865
  ${transcript}`,
@@ -22249,6 +23126,7 @@ Marvin \u2014 ${persona.name}
22249
23126
  "mcp__marvin-governance__get_dashboard_gar",
22250
23127
  "mcp__marvin-governance__get_dashboard_board",
22251
23128
  "mcp__marvin-governance__get_dashboard_upcoming",
23129
+ "mcp__marvin-governance__get_dashboard_sprint_summary",
22252
23130
  ...pluginTools.map((t) => `mcp__marvin-governance__${t.name}`),
22253
23131
  ...codeSkillTools.map((t) => `mcp__marvin-governance__${t.name}`)
22254
23132
  ]
@@ -22259,7 +23137,7 @@ Marvin \u2014 ${persona.name}
22259
23137
  if (existingSession) {
22260
23138
  queryOptions.resume = existingSession.id;
22261
23139
  }
22262
- const conversation = query2({
23140
+ const conversation = query3({
22263
23141
  prompt,
22264
23142
  options: queryOptions
22265
23143
  });
@@ -22618,7 +23496,7 @@ import * as fs12 from "fs";
22618
23496
  import * as path12 from "path";
22619
23497
  import chalk7 from "chalk";
22620
23498
  import ora2 from "ora";
22621
- import { query as query3 } from "@anthropic-ai/claude-agent-sdk";
23499
+ import { query as query4 } from "@anthropic-ai/claude-agent-sdk";
22622
23500
 
22623
23501
  // src/sources/prompts.ts
22624
23502
  function buildIngestSystemPrompt(persona, projectConfig, isDraft) {
@@ -22751,7 +23629,7 @@ async function ingestFile(options) {
22751
23629
  const spinner = ora2({ text: `Analyzing ${fileName}...`, color: "cyan" });
22752
23630
  spinner.start();
22753
23631
  try {
22754
- const conversation = query3({
23632
+ const conversation = query4({
22755
23633
  prompt: userPrompt,
22756
23634
  options: {
22757
23635
  systemPrompt,
@@ -23281,7 +24159,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
23281
24159
  import { tool as tool23 } from "@anthropic-ai/claude-agent-sdk";
23282
24160
 
23283
24161
  // src/skills/action-runner.ts
23284
- import { query as query4 } from "@anthropic-ai/claude-agent-sdk";
24162
+ import { query as query5 } from "@anthropic-ai/claude-agent-sdk";
23285
24163
  var GOVERNANCE_TOOL_NAMES2 = [
23286
24164
  "mcp__marvin-governance__list_decisions",
23287
24165
  "mcp__marvin-governance__get_decision",
@@ -23303,7 +24181,7 @@ async function runSkillAction(action, userPrompt, context) {
23303
24181
  try {
23304
24182
  const mcpServer = createMarvinMcpServer(context.store);
23305
24183
  const allowedTools = action.allowGovernanceTools !== false ? GOVERNANCE_TOOL_NAMES2 : [];
23306
- const conversation = query4({
24184
+ const conversation = query5({
23307
24185
  prompt: userPrompt,
23308
24186
  options: {
23309
24187
  systemPrompt: action.systemPrompt,
@@ -24344,7 +25222,7 @@ import chalk13 from "chalk";
24344
25222
  // src/analysis/analyze.ts
24345
25223
  import chalk12 from "chalk";
24346
25224
  import ora4 from "ora";
24347
- import { query as query5 } from "@anthropic-ai/claude-agent-sdk";
25225
+ import { query as query6 } from "@anthropic-ai/claude-agent-sdk";
24348
25226
 
24349
25227
  // src/analysis/prompts.ts
24350
25228
  function buildAnalyzeSystemPrompt(persona, projectConfig, isDraft) {
@@ -24474,7 +25352,7 @@ async function analyzeMeeting(options) {
24474
25352
  const spinner = ora4({ text: `Analyzing meeting ${meetingId}...`, color: "cyan" });
24475
25353
  spinner.start();
24476
25354
  try {
24477
- const conversation = query5({
25355
+ const conversation = query6({
24478
25356
  prompt: userPrompt,
24479
25357
  options: {
24480
25358
  systemPrompt,
@@ -24601,7 +25479,7 @@ import chalk15 from "chalk";
24601
25479
  // src/contributions/contribute.ts
24602
25480
  import chalk14 from "chalk";
24603
25481
  import ora5 from "ora";
24604
- import { query as query6 } from "@anthropic-ai/claude-agent-sdk";
25482
+ import { query as query7 } from "@anthropic-ai/claude-agent-sdk";
24605
25483
 
24606
25484
  // src/contributions/prompts.ts
24607
25485
  function buildContributeSystemPrompt(persona, contributionType, projectConfig, isDraft) {
@@ -24855,7 +25733,7 @@ async function contributeFromPersona(options) {
24855
25733
  "mcp__marvin-governance__get_action",
24856
25734
  "mcp__marvin-governance__get_question"
24857
25735
  ];
24858
- const conversation = query6({
25736
+ const conversation = query7({
24859
25737
  prompt: userPrompt,
24860
25738
  options: {
24861
25739
  systemPrompt,
@@ -25001,6 +25879,9 @@ Contribution: ${options.type}`));
25001
25879
  });
25002
25880
  }
25003
25881
 
25882
+ // src/cli/commands/report.ts
25883
+ import ora6 from "ora";
25884
+
25004
25885
  // src/reports/gar/render-ascii.ts
25005
25886
  import chalk16 from "chalk";
25006
25887
  var STATUS_DOT = {
@@ -25195,6 +26076,47 @@ async function healthReportCommand(options) {
25195
26076
  console.log(renderAscii2(report));
25196
26077
  }
25197
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
+ }
25198
26120
 
25199
26121
  // src/cli/commands/web.ts
25200
26122
  async function webCommand(options) {
@@ -25237,7 +26159,7 @@ function createProgram() {
25237
26159
  const program2 = new Command();
25238
26160
  program2.name("marvin").description(
25239
26161
  "AI-powered product development assistant with Product Owner, Delivery Manager, and Technical Lead personas"
25240
- ).version("0.4.5");
26162
+ ).version("0.4.7");
25241
26163
  program2.command("init").description("Initialize a new Marvin project in the current directory").action(async () => {
25242
26164
  await initCommand();
25243
26165
  });
@@ -25320,6 +26242,9 @@ function createProgram() {
25320
26242
  ).action(async (options) => {
25321
26243
  await healthReportCommand(options);
25322
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
+ });
25323
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) => {
25324
26249
  await webCommand(options);
25325
26250
  });