mrvn-cli 0.4.6 → 0.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/marvin.js CHANGED
@@ -14495,6 +14495,385 @@ 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 primaryDocs = workItemDocs.filter((d) => d.frontmatter.type !== "contribution");
14577
+ const byStatus = {};
14578
+ const byType = {};
14579
+ let doneCount = 0;
14580
+ let inProgressCount = 0;
14581
+ let openCount = 0;
14582
+ let blockedCount = 0;
14583
+ for (const doc of primaryDocs) {
14584
+ const s = doc.frontmatter.status;
14585
+ byStatus[s] = (byStatus[s] ?? 0) + 1;
14586
+ byType[doc.frontmatter.type] = (byType[doc.frontmatter.type] ?? 0) + 1;
14587
+ if (DONE_STATUSES.has(s)) doneCount++;
14588
+ else if (s === "in-progress") inProgressCount++;
14589
+ else if (s === "blocked") blockedCount++;
14590
+ else openCount++;
14591
+ }
14592
+ const allItemsById = /* @__PURE__ */ new Map();
14593
+ const childrenByParent = /* @__PURE__ */ new Map();
14594
+ const sprintItemIds = new Set(workItemDocs.map((d) => d.frontmatter.id));
14595
+ for (const doc of workItemDocs) {
14596
+ const about = doc.frontmatter.aboutArtifact;
14597
+ const item = {
14598
+ id: doc.frontmatter.id,
14599
+ title: doc.frontmatter.title,
14600
+ type: doc.frontmatter.type,
14601
+ status: doc.frontmatter.status,
14602
+ aboutArtifact: about
14603
+ };
14604
+ allItemsById.set(item.id, item);
14605
+ if (about && sprintItemIds.has(about)) {
14606
+ if (!childrenByParent.has(about)) childrenByParent.set(about, []);
14607
+ childrenByParent.get(about).push(item);
14608
+ }
14609
+ }
14610
+ const itemsWithChildren = /* @__PURE__ */ new Set();
14611
+ for (const [parentId, children] of childrenByParent) {
14612
+ const parent = allItemsById.get(parentId);
14613
+ if (parent) {
14614
+ parent.children = children;
14615
+ for (const child of children) itemsWithChildren.add(child.id);
14616
+ }
14617
+ }
14618
+ for (const item of allItemsById.values()) {
14619
+ if (item.children) {
14620
+ for (const child of item.children) {
14621
+ const grandchildren = childrenByParent.get(child.id);
14622
+ if (grandchildren) {
14623
+ child.children = grandchildren;
14624
+ for (const gc of grandchildren) itemsWithChildren.add(gc.id);
14625
+ }
14626
+ }
14627
+ }
14628
+ }
14629
+ const items = [];
14630
+ for (const doc of workItemDocs) {
14631
+ if (!itemsWithChildren.has(doc.frontmatter.id)) {
14632
+ items.push(allItemsById.get(doc.frontmatter.id));
14633
+ }
14634
+ }
14635
+ const workItems = {
14636
+ total: primaryDocs.length,
14637
+ done: doneCount,
14638
+ inProgress: inProgressCount,
14639
+ open: openCount,
14640
+ blocked: blockedCount,
14641
+ completionPct: primaryDocs.length > 0 ? Math.round(doneCount / primaryDocs.length * 100) : 0,
14642
+ byStatus,
14643
+ byType,
14644
+ items
14645
+ };
14646
+ const meetings = [];
14647
+ if (startDate && endDate) {
14648
+ const meetingDocs = allDocs.filter((d) => d.frontmatter.type === "meeting");
14649
+ for (const m of meetingDocs) {
14650
+ const meetingDate = m.frontmatter.date ?? m.frontmatter.created.slice(0, 10);
14651
+ if (meetingDate >= startDate && meetingDate <= endDate) {
14652
+ meetings.push({
14653
+ id: m.frontmatter.id,
14654
+ title: m.frontmatter.title,
14655
+ date: meetingDate
14656
+ });
14657
+ }
14658
+ }
14659
+ meetings.sort((a, b) => a.date.localeCompare(b.date));
14660
+ }
14661
+ const artifacts = [];
14662
+ if (startDate && endDate) {
14663
+ for (const doc of allDocs) {
14664
+ if (doc.frontmatter.type === "sprint") continue;
14665
+ const created = doc.frontmatter.created.slice(0, 10);
14666
+ const updated = doc.frontmatter.updated.slice(0, 10);
14667
+ if (created >= startDate && created <= endDate) {
14668
+ artifacts.push({
14669
+ id: doc.frontmatter.id,
14670
+ title: doc.frontmatter.title,
14671
+ type: doc.frontmatter.type,
14672
+ action: "created",
14673
+ date: created
14674
+ });
14675
+ } else if (updated >= startDate && updated <= endDate && updated !== created) {
14676
+ artifacts.push({
14677
+ id: doc.frontmatter.id,
14678
+ title: doc.frontmatter.title,
14679
+ type: doc.frontmatter.type,
14680
+ action: "updated",
14681
+ date: updated
14682
+ });
14683
+ }
14684
+ }
14685
+ artifacts.sort((a, b) => b.date.localeCompare(a.date));
14686
+ }
14687
+ const relevantTags = /* @__PURE__ */ new Set([sprintTag, ...linkedEpicIds.map((id) => `epic:${id}`)]);
14688
+ const openActions = allDocs.filter(
14689
+ (d) => d.frontmatter.type === "action" && !DONE_STATUSES.has(d.frontmatter.status) && d.frontmatter.tags?.some((t) => relevantTags.has(t))
14690
+ ).map((d) => ({
14691
+ id: d.frontmatter.id,
14692
+ title: d.frontmatter.title,
14693
+ owner: d.frontmatter.owner,
14694
+ dueDate: d.frontmatter.dueDate
14695
+ }));
14696
+ const openQuestions = allDocs.filter(
14697
+ (d) => d.frontmatter.type === "question" && d.frontmatter.status === "open" && d.frontmatter.tags?.some((t) => relevantTags.has(t))
14698
+ ).map((d) => ({
14699
+ id: d.frontmatter.id,
14700
+ title: d.frontmatter.title
14701
+ }));
14702
+ const blockers = allDocs.filter(
14703
+ (d) => d.frontmatter.status === "blocked" && d.frontmatter.tags?.includes(sprintTag)
14704
+ ).map((d) => ({
14705
+ id: d.frontmatter.id,
14706
+ title: d.frontmatter.title,
14707
+ type: d.frontmatter.type
14708
+ }));
14709
+ const riskBlockers = allDocs.filter(
14710
+ (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)
14711
+ );
14712
+ for (const d of riskBlockers) {
14713
+ blockers.push({
14714
+ id: d.frontmatter.id,
14715
+ title: d.frontmatter.title,
14716
+ type: d.frontmatter.type
14717
+ });
14718
+ }
14719
+ let velocity = null;
14720
+ const currentRate = workItems.completionPct;
14721
+ 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 ?? ""));
14722
+ if (completedSprints.length > 0) {
14723
+ const prev = completedSprints[0];
14724
+ const prevTag = `sprint:${prev.frontmatter.id}`;
14725
+ const prevWorkItems = allDocs.filter(
14726
+ (d) => d.frontmatter.type !== "sprint" && d.frontmatter.type !== "epic" && d.frontmatter.type !== "contribution" && d.frontmatter.tags?.includes(prevTag)
14727
+ );
14728
+ const prevDone = prevWorkItems.filter((d) => DONE_STATUSES.has(d.frontmatter.status)).length;
14729
+ const prevRate = prevWorkItems.length > 0 ? Math.round(prevDone / prevWorkItems.length * 100) : 0;
14730
+ velocity = {
14731
+ currentCompletionRate: currentRate,
14732
+ previousSprintRate: prevRate,
14733
+ previousSprintId: prev.frontmatter.id
14734
+ };
14735
+ } else {
14736
+ velocity = { currentCompletionRate: currentRate };
14737
+ }
14738
+ return {
14739
+ sprint: {
14740
+ id: fm.id,
14741
+ title: fm.title,
14742
+ goal: fm.goal,
14743
+ status: fm.status,
14744
+ startDate,
14745
+ endDate
14746
+ },
14747
+ timeline: { daysElapsed, daysRemaining, totalDays, percentComplete },
14748
+ linkedEpics,
14749
+ workItems,
14750
+ meetings,
14751
+ artifacts,
14752
+ openActions,
14753
+ openQuestions,
14754
+ blockers,
14755
+ velocity
14756
+ };
14757
+ }
14758
+
14759
+ // src/reports/sprint-summary/generator.ts
14760
+ import { query } from "@anthropic-ai/claude-agent-sdk";
14761
+ async function generateSprintSummary(data) {
14762
+ const prompt = buildPrompt(data);
14763
+ const result = query({
14764
+ prompt,
14765
+ options: {
14766
+ systemPrompt: SYSTEM_PROMPT,
14767
+ maxTurns: 1,
14768
+ tools: [],
14769
+ allowedTools: []
14770
+ }
14771
+ });
14772
+ for await (const msg of result) {
14773
+ if (msg.type === "assistant") {
14774
+ const text = msg.message.content.find(
14775
+ (b) => b.type === "text"
14776
+ );
14777
+ if (text) return text.text;
14778
+ }
14779
+ }
14780
+ return "Unable to generate sprint summary.";
14781
+ }
14782
+ 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:
14783
+
14784
+ ## Sprint Health
14785
+ One-line verdict on overall sprint health (healthy / at risk / behind).
14786
+
14787
+ ## Goal Progress
14788
+ How close the team is to achieving the sprint goal. Reference the goal text and completion metrics.
14789
+
14790
+ ## Key Achievements
14791
+ Notable completions, decisions made, meetings held during the sprint. Use bullet points.
14792
+
14793
+ ## Current Risks
14794
+ Blockers, overdue items, unresolved questions, items without owners. Use bullet points. If none, say so.
14795
+
14796
+ ## Outcome Projection
14797
+ Given the current pace and time remaining, what's the likely outcome? Will the sprint goal be met?
14798
+
14799
+ Be specific \u2014 reference artifact IDs, dates, and numbers from the data. Keep the tone professional but direct.`;
14800
+ function buildPrompt(data) {
14801
+ const sections = [];
14802
+ sections.push(`# Sprint: ${data.sprint.id} \u2014 ${data.sprint.title}`);
14803
+ sections.push(`Status: ${data.sprint.status}`);
14804
+ if (data.sprint.goal) sections.push(`Goal: ${data.sprint.goal}`);
14805
+ if (data.sprint.startDate) sections.push(`Start: ${data.sprint.startDate}`);
14806
+ if (data.sprint.endDate) sections.push(`End: ${data.sprint.endDate}`);
14807
+ sections.push(`
14808
+ ## Timeline`);
14809
+ sections.push(`Days elapsed: ${data.timeline.daysElapsed} / ${data.timeline.totalDays}`);
14810
+ sections.push(`Days remaining: ${data.timeline.daysRemaining}`);
14811
+ sections.push(`Timeline progress: ${data.timeline.percentComplete}%`);
14812
+ sections.push(`
14813
+ ## Work Items`);
14814
+ sections.push(`Total: ${data.workItems.total}, Done: ${data.workItems.done}, In Progress: ${data.workItems.inProgress}, Open: ${data.workItems.open}, Blocked: ${data.workItems.blocked}`);
14815
+ sections.push(`Completion: ${data.workItems.completionPct}%`);
14816
+ if (Object.keys(data.workItems.byType).length > 0) {
14817
+ sections.push(`By type: ${Object.entries(data.workItems.byType).map(([t, n]) => `${t}: ${n}`).join(", ")}`);
14818
+ }
14819
+ if (data.linkedEpics.length > 0) {
14820
+ sections.push(`
14821
+ ## Linked Epics`);
14822
+ for (const e of data.linkedEpics) {
14823
+ sections.push(`- ${e.id}: ${e.title} [${e.status}] \u2014 ${e.tasksDone}/${e.tasksTotal} tasks done`);
14824
+ }
14825
+ }
14826
+ if (data.meetings.length > 0) {
14827
+ sections.push(`
14828
+ ## Meetings During Sprint`);
14829
+ for (const m of data.meetings) {
14830
+ sections.push(`- ${m.date}: ${m.id} \u2014 ${m.title}`);
14831
+ }
14832
+ }
14833
+ if (data.artifacts.length > 0) {
14834
+ sections.push(`
14835
+ ## Artifacts Created/Updated During Sprint`);
14836
+ for (const a of data.artifacts.slice(0, 20)) {
14837
+ sections.push(`- ${a.date}: ${a.id} (${a.type}) ${a.action} \u2014 ${a.title}`);
14838
+ }
14839
+ if (data.artifacts.length > 20) {
14840
+ sections.push(`... and ${data.artifacts.length - 20} more`);
14841
+ }
14842
+ }
14843
+ if (data.openActions.length > 0) {
14844
+ sections.push(`
14845
+ ## Open Actions`);
14846
+ for (const a of data.openActions) {
14847
+ const owner = a.owner ?? "unowned";
14848
+ const due = a.dueDate ?? "no due date";
14849
+ sections.push(`- ${a.id}: ${a.title} (${owner}, ${due})`);
14850
+ }
14851
+ }
14852
+ if (data.openQuestions.length > 0) {
14853
+ sections.push(`
14854
+ ## Open Questions`);
14855
+ for (const q of data.openQuestions) {
14856
+ sections.push(`- ${q.id}: ${q.title}`);
14857
+ }
14858
+ }
14859
+ if (data.blockers.length > 0) {
14860
+ sections.push(`
14861
+ ## Blockers`);
14862
+ for (const b of data.blockers) {
14863
+ sections.push(`- ${b.id} (${b.type}): ${b.title}`);
14864
+ }
14865
+ }
14866
+ if (data.velocity) {
14867
+ sections.push(`
14868
+ ## Velocity`);
14869
+ sections.push(`Current sprint completion rate: ${data.velocity.currentCompletionRate}%`);
14870
+ if (data.velocity.previousSprintRate !== void 0) {
14871
+ sections.push(`Previous sprint (${data.velocity.previousSprintId}): ${data.velocity.previousSprintRate}%`);
14872
+ }
14873
+ }
14874
+ return sections.join("\n");
14875
+ }
14876
+
14498
14877
  // src/plugins/builtin/tools/reports.ts
14499
14878
  function createReportTools(store) {
14500
14879
  return [
@@ -14786,6 +15165,25 @@ function createReportTools(store) {
14786
15165
  },
14787
15166
  { annotations: { readOnlyHint: true } }
14788
15167
  ),
15168
+ tool2(
15169
+ "generate_sprint_summary",
15170
+ "Generate an AI-powered narrative summary of a sprint's progress, health, achievements, risks, and projected outcome",
15171
+ {
15172
+ sprint: external_exports.string().optional().describe("Sprint ID (e.g. 'SP-001'). Omit for the active sprint.")
15173
+ },
15174
+ async (args) => {
15175
+ const data = collectSprintSummaryData(store, args.sprint);
15176
+ if (!data) {
15177
+ const msg = args.sprint ? `Sprint ${args.sprint} not found.` : "No active sprint found.";
15178
+ return { content: [{ type: "text", text: msg }], isError: true };
15179
+ }
15180
+ const summary = await generateSprintSummary(data);
15181
+ return {
15182
+ content: [{ type: "text", text: summary }]
15183
+ };
15184
+ },
15185
+ { annotations: { readOnlyHint: true } }
15186
+ ),
14789
15187
  tool2(
14790
15188
  "save_report",
14791
15189
  "Save a generated report as a persistent document",
@@ -15218,18 +15616,18 @@ function createContributionTools(store) {
15218
15616
  content: external_exports.string().describe("Contribution content \u2014 the input from the persona"),
15219
15617
  persona: external_exports.string().describe("Persona making the contribution (e.g. 'tech-lead')"),
15220
15618
  contributionType: external_exports.string().describe("Type of contribution (e.g. 'action-result', 'risk-finding')"),
15221
- aboutArtifact: external_exports.string().optional().describe("Artifact ID this contribution relates to (e.g. 'A-001')"),
15222
- status: external_exports.string().optional().describe("Status (default: 'open')"),
15619
+ aboutArtifact: external_exports.string().describe("Artifact ID this contribution relates to (e.g. 'A-001', 'T-003')"),
15620
+ status: external_exports.string().optional().describe("Status (default: 'done')"),
15223
15621
  tags: external_exports.array(external_exports.string()).optional().describe("Tags for categorization")
15224
15622
  },
15225
15623
  async (args) => {
15226
15624
  const frontmatter = {
15227
15625
  title: args.title,
15228
- status: args.status ?? "open",
15626
+ status: args.status ?? "done",
15229
15627
  persona: args.persona,
15230
15628
  contributionType: args.contributionType
15231
15629
  };
15232
- if (args.aboutArtifact) frontmatter.aboutArtifact = args.aboutArtifact;
15630
+ frontmatter.aboutArtifact = args.aboutArtifact;
15233
15631
  if (args.tags) frontmatter.tags = args.tags;
15234
15632
  const doc = store.create("contribution", frontmatter, args.content);
15235
15633
  return {
@@ -15629,26 +16027,6 @@ function createSprintPlanningTools(store) {
15629
16027
 
15630
16028
  // src/plugins/builtin/tools/tasks.ts
15631
16029
  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
16030
  var linkedEpicArray = external_exports.preprocess(
15653
16031
  (val) => {
15654
16032
  if (typeof val === "string") {
@@ -15733,6 +16111,7 @@ function createTaskTools(store) {
15733
16111
  title: external_exports.string().describe("Task title"),
15734
16112
  content: external_exports.string().describe("Task description and implementation details"),
15735
16113
  linkedEpic: linkedEpicArray.describe("Epic ID(s) to link this task to (e.g. ['E-001'] or ['E-001', 'E-002'])"),
16114
+ aboutArtifact: external_exports.string().optional().describe("Parent artifact this task derives from (e.g. 'A-001')"),
15736
16115
  status: external_exports.enum(["backlog", "ready", "in-progress", "review", "done"]).optional().describe("Task status (default: 'backlog')"),
15737
16116
  acceptanceCriteria: external_exports.string().optional().describe("Acceptance criteria for the task"),
15738
16117
  technicalNotes: external_exports.string().optional().describe("Technical implementation notes"),
@@ -15758,6 +16137,7 @@ function createTaskTools(store) {
15758
16137
  linkedEpic: linkedEpics,
15759
16138
  tags: [...generateEpicTags(linkedEpics), ...args.tags ?? []]
15760
16139
  };
16140
+ if (args.aboutArtifact) frontmatter.aboutArtifact = args.aboutArtifact;
15761
16141
  if (args.acceptanceCriteria) frontmatter.acceptanceCriteria = args.acceptanceCriteria;
15762
16142
  if (args.technicalNotes) frontmatter.technicalNotes = args.technicalNotes;
15763
16143
  if (args.estimatedPoints !== void 0) frontmatter.estimatedPoints = args.estimatedPoints;
@@ -15781,6 +16161,7 @@ function createTaskTools(store) {
15781
16161
  {
15782
16162
  id: external_exports.string().describe("Task ID to update"),
15783
16163
  title: external_exports.string().optional().describe("New title"),
16164
+ aboutArtifact: external_exports.string().optional().describe("Parent artifact this task derives from (e.g. 'A-001')"),
15784
16165
  status: external_exports.enum(["backlog", "ready", "in-progress", "review", "done"]).optional().describe("New status"),
15785
16166
  content: external_exports.string().optional().describe("New content"),
15786
16167
  linkedEpic: linkedEpicArray.optional().describe("New linked epic ID(s)"),
@@ -17243,7 +17624,7 @@ import * as readline from "readline";
17243
17624
  import chalk2 from "chalk";
17244
17625
  import ora from "ora";
17245
17626
  import {
17246
- query as query2
17627
+ query as query3
17247
17628
  } from "@anthropic-ai/claude-agent-sdk";
17248
17629
 
17249
17630
  // src/personas/prompt-builder.ts
@@ -17383,9 +17764,9 @@ var DocumentStore = class {
17383
17764
  }
17384
17765
  }
17385
17766
  }
17386
- list(query7) {
17767
+ list(query8) {
17387
17768
  const results = [];
17388
- const types = query7?.type ? [query7.type] : Object.keys(this.typeDirs);
17769
+ const types = query8?.type ? [query8.type] : Object.keys(this.typeDirs);
17389
17770
  for (const type of types) {
17390
17771
  const dirName = this.typeDirs[type];
17391
17772
  if (!dirName) continue;
@@ -17396,9 +17777,9 @@ var DocumentStore = class {
17396
17777
  const filePath = path6.join(dir, file2);
17397
17778
  const raw = fs6.readFileSync(filePath, "utf-8");
17398
17779
  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)))
17780
+ if (query8?.status && doc.frontmatter.status !== query8.status) continue;
17781
+ if (query8?.owner && doc.frontmatter.owner !== query8.owner) continue;
17782
+ if (query8?.tag && (!doc.frontmatter.tags || !doc.frontmatter.tags.includes(query8.tag)))
17402
17783
  continue;
17403
17784
  results.push(doc);
17404
17785
  }
@@ -18364,7 +18745,7 @@ function computeUrgency(dueDateStr, todayStr) {
18364
18745
  if (diffDays <= 14) return "upcoming";
18365
18746
  return "later";
18366
18747
  }
18367
- var DONE_STATUSES = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
18748
+ var DONE_STATUSES2 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
18368
18749
  function getUpcomingData(store) {
18369
18750
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
18370
18751
  const allDocs = store.list();
@@ -18373,7 +18754,7 @@ function getUpcomingData(store) {
18373
18754
  docById.set(doc.frontmatter.id, doc);
18374
18755
  }
18375
18756
  const actions = allDocs.filter(
18376
- (d) => d.frontmatter.type === "action" && !DONE_STATUSES.has(d.frontmatter.status)
18757
+ (d) => d.frontmatter.type === "action" && !DONE_STATUSES2.has(d.frontmatter.status)
18377
18758
  );
18378
18759
  const actionsWithDue = actions.filter((d) => d.frontmatter.dueDate);
18379
18760
  const sprints = allDocs.filter((d) => d.frontmatter.type === "sprint");
@@ -18437,7 +18818,7 @@ function getUpcomingData(store) {
18437
18818
  const sprintEnd = sprint.frontmatter.endDate;
18438
18819
  const sprintTaskDocs = getSprintTasks(sprint);
18439
18820
  for (const task of sprintTaskDocs) {
18440
- if (DONE_STATUSES.has(task.frontmatter.status)) continue;
18821
+ if (DONE_STATUSES2.has(task.frontmatter.status)) continue;
18441
18822
  const existing = taskSprintMap.get(task.frontmatter.id);
18442
18823
  if (!existing || sprintEnd < existing.sprintEnd) {
18443
18824
  taskSprintMap.set(task.frontmatter.id, { task, sprint, sprintEnd });
@@ -18454,7 +18835,7 @@ function getUpcomingData(store) {
18454
18835
  urgency: computeUrgency(sprintEnd, today)
18455
18836
  })).sort((a, b) => a.sprintEndDate.localeCompare(b.sprintEndDate));
18456
18837
  const openItems = allDocs.filter(
18457
- (d) => ["action", "question", "task"].includes(d.frontmatter.type) && !DONE_STATUSES.has(d.frontmatter.status)
18838
+ (d) => ["action", "question", "task"].includes(d.frontmatter.type) && !DONE_STATUSES2.has(d.frontmatter.status)
18458
18839
  );
18459
18840
  const fourteenDaysAgo = new Date(todayMs - fourteenDaysMs).toISOString().slice(0, 10);
18460
18841
  const recentMeetings = allDocs.filter(
@@ -18552,6 +18933,9 @@ function getUpcomingData(store) {
18552
18933
  }).filter((item) => item.score > 0).sort((a, b) => b.score - a.score).slice(0, 15);
18553
18934
  return { dueSoonActions, dueSoonSprintTasks, trending };
18554
18935
  }
18936
+ function getSprintSummaryData(store, sprintId) {
18937
+ return collectSprintSummaryData(store, sprintId);
18938
+ }
18555
18939
 
18556
18940
  // src/web/templates/layout.ts
18557
18941
  function collapsibleSection(sectionId, title, content, opts) {
@@ -18693,6 +19077,7 @@ function layout(opts, body) {
18693
19077
  const topItems = [
18694
19078
  { href: "/", label: "Overview" },
18695
19079
  { href: "/upcoming", label: "Upcoming" },
19080
+ { href: "/sprint-summary", label: "Sprint Summary" },
18696
19081
  { href: "/timeline", label: "Timeline" },
18697
19082
  { href: "/board", label: "Board" },
18698
19083
  { href: "/gar", label: "GAR Report" },
@@ -19076,6 +19461,17 @@ tr:hover td {
19076
19461
  background: var(--bg-hover);
19077
19462
  }
19078
19463
 
19464
+ /* Hierarchical work-item sub-rows */
19465
+ .child-row td {
19466
+ font-size: 0.8125rem;
19467
+ border-bottom-style: dashed;
19468
+ }
19469
+ .contribution-row td {
19470
+ font-size: 0.8125rem;
19471
+ color: var(--text-dim);
19472
+ border-bottom-style: dashed;
19473
+ }
19474
+
19079
19475
  /* GAR */
19080
19476
  .gar-overall {
19081
19477
  text-align: center;
@@ -19703,6 +20099,112 @@ tr:hover td {
19703
20099
 
19704
20100
  .text-dim { color: var(--text-dim); }
19705
20101
 
20102
+ /* Sprint Summary */
20103
+ .sprint-goal {
20104
+ background: var(--bg-card);
20105
+ border: 1px solid var(--border);
20106
+ border-radius: var(--radius);
20107
+ padding: 0.75rem 1rem;
20108
+ margin-bottom: 1rem;
20109
+ font-size: 0.9rem;
20110
+ color: var(--text);
20111
+ }
20112
+
20113
+ .sprint-progress-bar {
20114
+ position: relative;
20115
+ height: 24px;
20116
+ background: var(--bg-card);
20117
+ border: 1px solid var(--border);
20118
+ border-radius: 12px;
20119
+ margin-bottom: 1.25rem;
20120
+ overflow: hidden;
20121
+ }
20122
+
20123
+ .sprint-progress-fill {
20124
+ height: 100%;
20125
+ background: linear-gradient(90deg, var(--accent-dim), var(--accent));
20126
+ border-radius: 12px;
20127
+ transition: width 0.3s ease;
20128
+ }
20129
+
20130
+ .sprint-progress-label {
20131
+ position: absolute;
20132
+ top: 50%;
20133
+ left: 50%;
20134
+ transform: translate(-50%, -50%);
20135
+ font-size: 0.7rem;
20136
+ font-weight: 700;
20137
+ color: var(--text);
20138
+ }
20139
+
20140
+ .sprint-ai-section {
20141
+ margin-top: 2rem;
20142
+ background: var(--bg-card);
20143
+ border: 1px solid var(--border);
20144
+ border-radius: var(--radius);
20145
+ padding: 1.5rem;
20146
+ }
20147
+
20148
+ .sprint-ai-section h3 {
20149
+ font-size: 1rem;
20150
+ font-weight: 600;
20151
+ margin-bottom: 0.5rem;
20152
+ }
20153
+
20154
+ .sprint-generate-btn {
20155
+ background: var(--accent);
20156
+ color: #fff;
20157
+ border: none;
20158
+ border-radius: var(--radius);
20159
+ padding: 0.5rem 1.25rem;
20160
+ font-size: 0.85rem;
20161
+ font-weight: 600;
20162
+ cursor: pointer;
20163
+ margin-top: 0.75rem;
20164
+ transition: background 0.15s;
20165
+ }
20166
+
20167
+ .sprint-generate-btn:hover:not(:disabled) {
20168
+ background: var(--accent-dim);
20169
+ }
20170
+
20171
+ .sprint-generate-btn:disabled {
20172
+ opacity: 0.5;
20173
+ cursor: not-allowed;
20174
+ }
20175
+
20176
+ .sprint-loading {
20177
+ display: flex;
20178
+ align-items: center;
20179
+ gap: 0.75rem;
20180
+ padding: 1rem 0;
20181
+ color: var(--text-dim);
20182
+ font-size: 0.85rem;
20183
+ }
20184
+
20185
+ .sprint-spinner {
20186
+ width: 20px;
20187
+ height: 20px;
20188
+ border: 2px solid var(--border);
20189
+ border-top-color: var(--accent);
20190
+ border-radius: 50%;
20191
+ animation: sprint-spin 0.8s linear infinite;
20192
+ }
20193
+
20194
+ @keyframes sprint-spin {
20195
+ to { transform: rotate(360deg); }
20196
+ }
20197
+
20198
+ .sprint-error {
20199
+ color: var(--red);
20200
+ font-size: 0.85rem;
20201
+ padding: 0.5rem 0;
20202
+ }
20203
+
20204
+ .sprint-ai-section .detail-content {
20205
+ margin-top: 1rem;
20206
+ }
20207
+
19706
20208
  /* Collapsible sections */
19707
20209
  .collapsible-header {
19708
20210
  cursor: pointer;
@@ -20594,7 +21096,211 @@ function upcomingPage(data) {
20594
21096
  `;
20595
21097
  }
20596
21098
 
21099
+ // src/web/templates/pages/sprint-summary.ts
21100
+ function progressBar(pct) {
21101
+ return `<div class="sprint-progress-bar">
21102
+ <div class="sprint-progress-fill" style="width: ${pct}%"></div>
21103
+ <span class="sprint-progress-label">${pct}%</span>
21104
+ </div>`;
21105
+ }
21106
+ function sprintSummaryPage(data, cached2) {
21107
+ if (!data) {
21108
+ return `
21109
+ <div class="page-header">
21110
+ <h2>Sprint Summary</h2>
21111
+ <div class="subtitle">AI-powered sprint narrative</div>
21112
+ </div>
21113
+ <div class="empty">
21114
+ <h3>No Active Sprint</h3>
21115
+ <p>No active sprint found. Create a sprint and set its status to "active" to see the summary.</p>
21116
+ </div>`;
21117
+ }
21118
+ const statsCards = `
21119
+ <div class="cards">
21120
+ <div class="card">
21121
+ <div class="card-label">Completion</div>
21122
+ <div class="card-value">${data.workItems.completionPct}%</div>
21123
+ <div class="card-sub">${data.workItems.done} / ${data.workItems.total} items done</div>
21124
+ </div>
21125
+ <div class="card">
21126
+ <div class="card-label">Days Remaining</div>
21127
+ <div class="card-value">${data.timeline.daysRemaining}</div>
21128
+ <div class="card-sub">${data.timeline.daysElapsed} of ${data.timeline.totalDays} elapsed</div>
21129
+ </div>
21130
+ <div class="card">
21131
+ <div class="card-label">Epics</div>
21132
+ <div class="card-value">${data.linkedEpics.length}</div>
21133
+ <div class="card-sub">linked to sprint</div>
21134
+ </div>
21135
+ <div class="card">
21136
+ <div class="card-label">Blockers</div>
21137
+ <div class="card-value${data.blockers.length > 0 ? " priority-high" : ""}">${data.blockers.length}</div>
21138
+ <div class="card-sub">${data.openActions.length} open actions</div>
21139
+ </div>
21140
+ </div>`;
21141
+ const epicsTable = data.linkedEpics.length > 0 ? collapsibleSection(
21142
+ "ss-epics",
21143
+ "Linked Epics",
21144
+ `<div class="table-wrap">
21145
+ <table>
21146
+ <thead>
21147
+ <tr><th>ID</th><th>Title</th><th>Status</th><th>Tasks</th></tr>
21148
+ </thead>
21149
+ <tbody>
21150
+ ${data.linkedEpics.map((e) => `
21151
+ <tr>
21152
+ <td><a href="/docs/epic/${escapeHtml(e.id)}">${escapeHtml(e.id)}</a></td>
21153
+ <td>${escapeHtml(e.title)}</td>
21154
+ <td>${statusBadge(e.status)}</td>
21155
+ <td>${e.tasksDone} / ${e.tasksTotal}</td>
21156
+ </tr>`).join("")}
21157
+ </tbody>
21158
+ </table>
21159
+ </div>`,
21160
+ { titleTag: "h3" }
21161
+ ) : "";
21162
+ function renderItemRows(items, depth = 0) {
21163
+ return items.flatMap((w) => {
21164
+ const isChild = depth > 0;
21165
+ const isContribution = w.type === "contribution";
21166
+ const rowClass = isContribution ? ' class="contribution-row"' : isChild ? ' class="child-row"' : "";
21167
+ const indent = depth > 0 ? ` style="padding-left: ${0.75 + depth * 1}rem"` : "";
21168
+ const row = `
21169
+ <tr${rowClass}>
21170
+ <td${indent}><a href="/docs/${escapeHtml(w.type)}/${escapeHtml(w.id)}">${escapeHtml(w.id)}</a></td>
21171
+ <td>${escapeHtml(w.title)}</td>
21172
+ <td>${escapeHtml(typeLabel(w.type))}</td>
21173
+ <td>${statusBadge(w.status)}</td>
21174
+ </tr>`;
21175
+ const childRows = w.children ? renderItemRows(w.children, depth + 1) : [];
21176
+ return [row, ...childRows];
21177
+ });
21178
+ }
21179
+ const workItemRows = renderItemRows(data.workItems.items);
21180
+ const workItemsSection = workItemRows.length > 0 ? collapsibleSection(
21181
+ "ss-work-items",
21182
+ "Work Items",
21183
+ `<div class="table-wrap">
21184
+ <table>
21185
+ <thead>
21186
+ <tr><th>ID</th><th>Title</th><th>Type</th><th>Status</th></tr>
21187
+ </thead>
21188
+ <tbody>
21189
+ ${workItemRows.join("")}
21190
+ </tbody>
21191
+ </table>
21192
+ </div>`,
21193
+ { titleTag: "h3", defaultCollapsed: true }
21194
+ ) : "";
21195
+ const activitySection = data.artifacts.length > 0 ? collapsibleSection(
21196
+ "ss-activity",
21197
+ "Recent Activity",
21198
+ `<div class="table-wrap">
21199
+ <table>
21200
+ <thead>
21201
+ <tr><th>Date</th><th>ID</th><th>Title</th><th>Type</th><th>Action</th></tr>
21202
+ </thead>
21203
+ <tbody>
21204
+ ${data.artifacts.slice(0, 15).map((a) => `
21205
+ <tr>
21206
+ <td>${formatDate(a.date)}</td>
21207
+ <td><a href="/docs/${escapeHtml(a.type)}/${escapeHtml(a.id)}">${escapeHtml(a.id)}</a></td>
21208
+ <td>${escapeHtml(a.title)}</td>
21209
+ <td>${escapeHtml(typeLabel(a.type))}</td>
21210
+ <td>${escapeHtml(a.action)}</td>
21211
+ </tr>`).join("")}
21212
+ </tbody>
21213
+ </table>
21214
+ </div>`,
21215
+ { titleTag: "h3", defaultCollapsed: true }
21216
+ ) : "";
21217
+ const meetingsSection = data.meetings.length > 0 ? collapsibleSection(
21218
+ "ss-meetings",
21219
+ `Meetings (${data.meetings.length})`,
21220
+ `<div class="table-wrap">
21221
+ <table>
21222
+ <thead>
21223
+ <tr><th>Date</th><th>ID</th><th>Title</th></tr>
21224
+ </thead>
21225
+ <tbody>
21226
+ ${data.meetings.map((m) => `
21227
+ <tr>
21228
+ <td>${formatDate(m.date)}</td>
21229
+ <td><a href="/docs/meeting/${escapeHtml(m.id)}">${escapeHtml(m.id)}</a></td>
21230
+ <td>${escapeHtml(m.title)}</td>
21231
+ </tr>`).join("")}
21232
+ </tbody>
21233
+ </table>
21234
+ </div>`,
21235
+ { titleTag: "h3", defaultCollapsed: true }
21236
+ ) : "";
21237
+ const goalHtml = data.sprint.goal ? `<div class="sprint-goal"><strong>Goal:</strong> ${escapeHtml(data.sprint.goal)}</div>` : "";
21238
+ const dateRange = data.sprint.startDate && data.sprint.endDate ? `<span class="text-dim">${formatDate(data.sprint.startDate)} \u2014 ${formatDate(data.sprint.endDate)}</span>` : "";
21239
+ return `
21240
+ <div class="page-header">
21241
+ <h2>${escapeHtml(data.sprint.id)} \u2014 ${escapeHtml(data.sprint.title)} ${statusBadge(data.sprint.status)}</h2>
21242
+ <div class="subtitle">Sprint Summary ${dateRange}</div>
21243
+ </div>
21244
+ ${goalHtml}
21245
+ ${progressBar(data.timeline.percentComplete)}
21246
+ ${statsCards}
21247
+ ${epicsTable}
21248
+ ${workItemsSection}
21249
+ ${activitySection}
21250
+ ${meetingsSection}
21251
+
21252
+ <div class="sprint-ai-section">
21253
+ <h3>AI Summary</h3>
21254
+ ${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>`}
21255
+ <button class="sprint-generate-btn" onclick="generateSummary()" id="generate-btn">${cached2 ? "Regenerate" : "Generate AI Summary"}</button>
21256
+ <div id="summary-loading" class="sprint-loading" style="display:none">
21257
+ <div class="sprint-spinner"></div>
21258
+ <span>Generating summary...</span>
21259
+ </div>
21260
+ <div id="summary-error" class="sprint-error" style="display:none"></div>
21261
+ <div id="summary-content" class="detail-content"${cached2 ? "" : ' style="display:none"'}>${cached2 ? cached2.html : ""}</div>
21262
+ </div>
21263
+
21264
+ <script>
21265
+ async function generateSummary() {
21266
+ var btn = document.getElementById('generate-btn');
21267
+ var loading = document.getElementById('summary-loading');
21268
+ var errorEl = document.getElementById('summary-error');
21269
+ var content = document.getElementById('summary-content');
21270
+
21271
+ btn.disabled = true;
21272
+ btn.style.display = 'none';
21273
+ loading.style.display = 'flex';
21274
+ errorEl.style.display = 'none';
21275
+ content.style.display = 'none';
21276
+
21277
+ try {
21278
+ var res = await fetch('/api/sprint-summary', {
21279
+ method: 'POST',
21280
+ headers: { 'Content-Type': 'application/json' },
21281
+ body: JSON.stringify({ sprintId: '${escapeHtml(data.sprint.id)}' })
21282
+ });
21283
+ var json = await res.json();
21284
+ if (!res.ok) throw new Error(json.error || 'Failed to generate summary');
21285
+ loading.style.display = 'none';
21286
+ content.innerHTML = json.html;
21287
+ content.style.display = 'block';
21288
+ btn.textContent = 'Regenerate';
21289
+ btn.style.display = '';
21290
+ btn.disabled = false;
21291
+ } catch (e) {
21292
+ loading.style.display = 'none';
21293
+ errorEl.textContent = e.message;
21294
+ errorEl.style.display = 'block';
21295
+ btn.style.display = '';
21296
+ btn.disabled = false;
21297
+ }
21298
+ }
21299
+ </script>`;
21300
+ }
21301
+
20597
21302
  // src/web/router.ts
21303
+ var sprintSummaryCache = /* @__PURE__ */ new Map();
20598
21304
  function handleRequest(req, res, store, projectName, navGroups) {
20599
21305
  const parsed = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
20600
21306
  const pathname = parsed.pathname;
@@ -20640,6 +21346,42 @@ function handleRequest(req, res, store, projectName, navGroups) {
20640
21346
  respond(res, layout({ title: "Upcoming", activePath: "/upcoming", projectName, navGroups }, body));
20641
21347
  return;
20642
21348
  }
21349
+ if (pathname === "/sprint-summary" && req.method === "GET") {
21350
+ const sprintId = parsed.searchParams.get("sprint") ?? void 0;
21351
+ const data = getSprintSummaryData(store, sprintId);
21352
+ const cached2 = data ? sprintSummaryCache.get(data.sprint.id) : void 0;
21353
+ const body = sprintSummaryPage(data, cached2 ? { html: cached2.html, generatedAt: cached2.generatedAt } : void 0);
21354
+ respond(res, layout({ title: "Sprint Summary", activePath: "/sprint-summary", projectName, navGroups }, body));
21355
+ return;
21356
+ }
21357
+ if (pathname === "/api/sprint-summary" && req.method === "POST") {
21358
+ let bodyStr = "";
21359
+ req.on("data", (chunk) => {
21360
+ bodyStr += chunk;
21361
+ });
21362
+ req.on("end", async () => {
21363
+ try {
21364
+ const { sprintId } = JSON.parse(bodyStr || "{}");
21365
+ const data = getSprintSummaryData(store, sprintId);
21366
+ if (!data) {
21367
+ res.writeHead(404, { "Content-Type": "application/json" });
21368
+ res.end(JSON.stringify({ error: "Sprint not found" }));
21369
+ return;
21370
+ }
21371
+ const summary = await generateSprintSummary(data);
21372
+ const html = renderMarkdown(summary);
21373
+ const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
21374
+ sprintSummaryCache.set(data.sprint.id, { html, generatedAt });
21375
+ res.writeHead(200, { "Content-Type": "application/json" });
21376
+ res.end(JSON.stringify({ summary, html, generatedAt }));
21377
+ } catch (err) {
21378
+ console.error("[marvin web] Sprint summary generation error:", err);
21379
+ res.writeHead(500, { "Content-Type": "application/json" });
21380
+ res.end(JSON.stringify({ error: "Failed to generate summary" }));
21381
+ }
21382
+ });
21383
+ return;
21384
+ }
20643
21385
  const boardMatch = pathname.match(/^\/board(?:\/([^/]+))?$/);
20644
21386
  if (boardMatch) {
20645
21387
  const type = boardMatch[1];
@@ -22135,6 +22877,24 @@ function createWebTools(store, projectName, navGroups) {
22135
22877
  };
22136
22878
  },
22137
22879
  { annotations: { readOnlyHint: true } }
22880
+ ),
22881
+ tool22(
22882
+ "get_dashboard_sprint_summary",
22883
+ "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.",
22884
+ {
22885
+ sprint: external_exports.string().optional().describe("Sprint ID (e.g. 'SP-001'). Omit for the active sprint.")
22886
+ },
22887
+ async (args) => {
22888
+ const data = getSprintSummaryData(store, args.sprint);
22889
+ if (!data) {
22890
+ const msg = args.sprint ? `Sprint ${args.sprint} not found.` : "No active sprint found.";
22891
+ return { content: [{ type: "text", text: msg }], isError: true };
22892
+ }
22893
+ return {
22894
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
22895
+ };
22896
+ },
22897
+ { annotations: { readOnlyHint: true } }
22138
22898
  )
22139
22899
  ];
22140
22900
  }
@@ -22160,11 +22920,11 @@ function createMarvinMcpServer(store, options) {
22160
22920
  }
22161
22921
 
22162
22922
  // src/agent/session-namer.ts
22163
- import { query } from "@anthropic-ai/claude-agent-sdk";
22923
+ import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
22164
22924
  async function generateSessionName(turns) {
22165
22925
  try {
22166
22926
  const transcript = turns.slice(-20).map((t) => `${t.role}: ${t.content.slice(0, 200)}`).join("\n");
22167
- const result = query({
22927
+ const result = query2({
22168
22928
  prompt: `Summarize this conversation in 3-5 words as a kebab-case name suitable for a filename. Output ONLY the name, nothing else.
22169
22929
 
22170
22930
  ${transcript}`,
@@ -22431,6 +23191,7 @@ Marvin \u2014 ${persona.name}
22431
23191
  "mcp__marvin-governance__get_dashboard_gar",
22432
23192
  "mcp__marvin-governance__get_dashboard_board",
22433
23193
  "mcp__marvin-governance__get_dashboard_upcoming",
23194
+ "mcp__marvin-governance__get_dashboard_sprint_summary",
22434
23195
  ...pluginTools.map((t) => `mcp__marvin-governance__${t.name}`),
22435
23196
  ...codeSkillTools.map((t) => `mcp__marvin-governance__${t.name}`)
22436
23197
  ]
@@ -22441,7 +23202,7 @@ Marvin \u2014 ${persona.name}
22441
23202
  if (existingSession) {
22442
23203
  queryOptions.resume = existingSession.id;
22443
23204
  }
22444
- const conversation = query2({
23205
+ const conversation = query3({
22445
23206
  prompt,
22446
23207
  options: queryOptions
22447
23208
  });
@@ -22800,7 +23561,7 @@ import * as fs12 from "fs";
22800
23561
  import * as path12 from "path";
22801
23562
  import chalk7 from "chalk";
22802
23563
  import ora2 from "ora";
22803
- import { query as query3 } from "@anthropic-ai/claude-agent-sdk";
23564
+ import { query as query4 } from "@anthropic-ai/claude-agent-sdk";
22804
23565
 
22805
23566
  // src/sources/prompts.ts
22806
23567
  function buildIngestSystemPrompt(persona, projectConfig, isDraft) {
@@ -22933,7 +23694,7 @@ async function ingestFile(options) {
22933
23694
  const spinner = ora2({ text: `Analyzing ${fileName}...`, color: "cyan" });
22934
23695
  spinner.start();
22935
23696
  try {
22936
- const conversation = query3({
23697
+ const conversation = query4({
22937
23698
  prompt: userPrompt,
22938
23699
  options: {
22939
23700
  systemPrompt,
@@ -23463,7 +24224,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
23463
24224
  import { tool as tool23 } from "@anthropic-ai/claude-agent-sdk";
23464
24225
 
23465
24226
  // src/skills/action-runner.ts
23466
- import { query as query4 } from "@anthropic-ai/claude-agent-sdk";
24227
+ import { query as query5 } from "@anthropic-ai/claude-agent-sdk";
23467
24228
  var GOVERNANCE_TOOL_NAMES2 = [
23468
24229
  "mcp__marvin-governance__list_decisions",
23469
24230
  "mcp__marvin-governance__get_decision",
@@ -23485,7 +24246,7 @@ async function runSkillAction(action, userPrompt, context) {
23485
24246
  try {
23486
24247
  const mcpServer = createMarvinMcpServer(context.store);
23487
24248
  const allowedTools = action.allowGovernanceTools !== false ? GOVERNANCE_TOOL_NAMES2 : [];
23488
- const conversation = query4({
24249
+ const conversation = query5({
23489
24250
  prompt: userPrompt,
23490
24251
  options: {
23491
24252
  systemPrompt: action.systemPrompt,
@@ -24526,7 +25287,7 @@ import chalk13 from "chalk";
24526
25287
  // src/analysis/analyze.ts
24527
25288
  import chalk12 from "chalk";
24528
25289
  import ora4 from "ora";
24529
- import { query as query5 } from "@anthropic-ai/claude-agent-sdk";
25290
+ import { query as query6 } from "@anthropic-ai/claude-agent-sdk";
24530
25291
 
24531
25292
  // src/analysis/prompts.ts
24532
25293
  function buildAnalyzeSystemPrompt(persona, projectConfig, isDraft) {
@@ -24656,7 +25417,7 @@ async function analyzeMeeting(options) {
24656
25417
  const spinner = ora4({ text: `Analyzing meeting ${meetingId}...`, color: "cyan" });
24657
25418
  spinner.start();
24658
25419
  try {
24659
- const conversation = query5({
25420
+ const conversation = query6({
24660
25421
  prompt: userPrompt,
24661
25422
  options: {
24662
25423
  systemPrompt,
@@ -24783,7 +25544,7 @@ import chalk15 from "chalk";
24783
25544
  // src/contributions/contribute.ts
24784
25545
  import chalk14 from "chalk";
24785
25546
  import ora5 from "ora";
24786
- import { query as query6 } from "@anthropic-ai/claude-agent-sdk";
25547
+ import { query as query7 } from "@anthropic-ai/claude-agent-sdk";
24787
25548
 
24788
25549
  // src/contributions/prompts.ts
24789
25550
  function buildContributeSystemPrompt(persona, contributionType, projectConfig, isDraft) {
@@ -25037,7 +25798,7 @@ async function contributeFromPersona(options) {
25037
25798
  "mcp__marvin-governance__get_action",
25038
25799
  "mcp__marvin-governance__get_question"
25039
25800
  ];
25040
- const conversation = query6({
25801
+ const conversation = query7({
25041
25802
  prompt: userPrompt,
25042
25803
  options: {
25043
25804
  systemPrompt,
@@ -25183,6 +25944,9 @@ Contribution: ${options.type}`));
25183
25944
  });
25184
25945
  }
25185
25946
 
25947
+ // src/cli/commands/report.ts
25948
+ import ora6 from "ora";
25949
+
25186
25950
  // src/reports/gar/render-ascii.ts
25187
25951
  import chalk16 from "chalk";
25188
25952
  var STATUS_DOT = {
@@ -25377,6 +26141,47 @@ async function healthReportCommand(options) {
25377
26141
  console.log(renderAscii2(report));
25378
26142
  }
25379
26143
  }
26144
+ async function sprintSummaryCommand(options) {
26145
+ const project = loadProject();
26146
+ const plugin = resolvePlugin(project.config.methodology);
26147
+ const pluginRegistrations = plugin?.documentTypeRegistrations ?? [];
26148
+ const allSkills = loadAllSkills(project.marvinDir);
26149
+ const allSkillIds = [...allSkills.keys()];
26150
+ const skillRegistrations = collectSkillRegistrations(allSkillIds, allSkills);
26151
+ const store = new DocumentStore(project.marvinDir, [...pluginRegistrations, ...skillRegistrations]);
26152
+ const data = collectSprintSummaryData(store, options.sprint);
26153
+ if (!data) {
26154
+ const msg = options.sprint ? `Sprint ${options.sprint} not found.` : "No active sprint found. Use --sprint <id> to specify one.";
26155
+ console.error(msg);
26156
+ process.exit(1);
26157
+ }
26158
+ const spinner = ora6({ text: "Generating AI sprint summary...", color: "cyan" }).start();
26159
+ try {
26160
+ const summary = await generateSprintSummary(data);
26161
+ spinner.stop();
26162
+ const header = `# Sprint Summary: ${data.sprint.id} \u2014 ${data.sprint.title}
26163
+
26164
+ `;
26165
+ console.log(header + summary);
26166
+ if (options.save) {
26167
+ const doc = store.create(
26168
+ "report",
26169
+ {
26170
+ title: `Sprint Summary: ${data.sprint.title}`,
26171
+ status: "final",
26172
+ tags: [`report-type:sprint-summary`, `sprint:${data.sprint.id}`]
26173
+ },
26174
+ summary
26175
+ );
26176
+ console.log(`
26177
+ Saved as ${doc.frontmatter.id}`);
26178
+ }
26179
+ } catch (err) {
26180
+ spinner.stop();
26181
+ console.error("Failed to generate sprint summary:", err);
26182
+ process.exit(1);
26183
+ }
26184
+ }
25380
26185
 
25381
26186
  // src/cli/commands/web.ts
25382
26187
  async function webCommand(options) {
@@ -25419,7 +26224,7 @@ function createProgram() {
25419
26224
  const program2 = new Command();
25420
26225
  program2.name("marvin").description(
25421
26226
  "AI-powered product development assistant with Product Owner, Delivery Manager, and Technical Lead personas"
25422
- ).version("0.4.6");
26227
+ ).version("0.4.8");
25423
26228
  program2.command("init").description("Initialize a new Marvin project in the current directory").action(async () => {
25424
26229
  await initCommand();
25425
26230
  });
@@ -25502,6 +26307,9 @@ function createProgram() {
25502
26307
  ).action(async (options) => {
25503
26308
  await healthReportCommand(options);
25504
26309
  });
26310
+ 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) => {
26311
+ await sprintSummaryCommand(options);
26312
+ });
25505
26313
  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
26314
  await webCommand(options);
25507
26315
  });