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