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