mrvn-cli 0.4.5 → 0.4.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1042 -115
- package/dist/index.js.map +1 -1
- package/dist/marvin-serve.js +1001 -124
- package/dist/marvin-serve.js.map +1 -1
- package/dist/marvin.js +1060 -135
- 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,8 +15925,28 @@ 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
|
|
15933
|
+
function collapsibleSection(sectionId, title, content, opts) {
|
|
15934
|
+
const tag = opts?.titleTag ?? "div";
|
|
15935
|
+
const cls = opts?.titleClass ?? "section-title";
|
|
15936
|
+
const collapsed = opts?.defaultCollapsed ? " collapsed" : "";
|
|
15937
|
+
return `
|
|
15938
|
+
<div class="collapsible${collapsed}" data-section-id="${escapeHtml(sectionId)}">
|
|
15939
|
+
<${tag} class="${cls} collapsible-header" onclick="toggleSection(this)">
|
|
15940
|
+
<svg class="collapsible-chevron" viewBox="0 0 16 16" width="14" height="14" fill="currentColor">
|
|
15941
|
+
<path d="M4.94 5.72a.75.75 0 0 1 1.06-.02L8 7.56l1.97-1.84a.75.75 0 1 1 1.02 1.1l-2.5 2.34a.75.75 0 0 1-1.02 0l-2.5-2.34a.75.75 0 0 1-.03-1.06z"/>
|
|
15942
|
+
</svg>
|
|
15943
|
+
<span>${title}</span>
|
|
15944
|
+
</${tag}>
|
|
15945
|
+
<div class="collapsible-body">
|
|
15946
|
+
${content}
|
|
15947
|
+
</div>
|
|
15948
|
+
</div>`;
|
|
15949
|
+
}
|
|
15726
15950
|
function escapeHtml(str) {
|
|
15727
15951
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
15728
15952
|
}
|
|
@@ -15845,6 +16069,7 @@ function layout(opts, body) {
|
|
|
15845
16069
|
const topItems = [
|
|
15846
16070
|
{ href: "/", label: "Overview" },
|
|
15847
16071
|
{ href: "/upcoming", label: "Upcoming" },
|
|
16072
|
+
{ href: "/sprint-summary", label: "Sprint Summary" },
|
|
15848
16073
|
{ href: "/timeline", label: "Timeline" },
|
|
15849
16074
|
{ href: "/board", label: "Board" },
|
|
15850
16075
|
{ href: "/gar", label: "GAR Report" },
|
|
@@ -15890,6 +16115,32 @@ function layout(opts, body) {
|
|
|
15890
16115
|
${body}
|
|
15891
16116
|
</main>
|
|
15892
16117
|
</div>
|
|
16118
|
+
<script>
|
|
16119
|
+
function toggleSection(header) {
|
|
16120
|
+
var section = header.closest('.collapsible');
|
|
16121
|
+
if (!section) return;
|
|
16122
|
+
section.classList.toggle('collapsed');
|
|
16123
|
+
var id = section.getAttribute('data-section-id');
|
|
16124
|
+
if (id) {
|
|
16125
|
+
try {
|
|
16126
|
+
var state = JSON.parse(localStorage.getItem('marvin-collapsed') || '{}');
|
|
16127
|
+
state[id] = section.classList.contains('collapsed');
|
|
16128
|
+
localStorage.setItem('marvin-collapsed', JSON.stringify(state));
|
|
16129
|
+
} catch(e) {}
|
|
16130
|
+
}
|
|
16131
|
+
}
|
|
16132
|
+
// Restore collapsed state on load
|
|
16133
|
+
(function() {
|
|
16134
|
+
try {
|
|
16135
|
+
var state = JSON.parse(localStorage.getItem('marvin-collapsed') || '{}');
|
|
16136
|
+
document.querySelectorAll('.collapsible[data-section-id]').forEach(function(el) {
|
|
16137
|
+
var id = el.getAttribute('data-section-id');
|
|
16138
|
+
if (state[id] === true) el.classList.add('collapsed');
|
|
16139
|
+
else if (state[id] === false) el.classList.remove('collapsed');
|
|
16140
|
+
});
|
|
16141
|
+
} catch(e) {}
|
|
16142
|
+
})();
|
|
16143
|
+
</script>
|
|
15893
16144
|
<script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
|
|
15894
16145
|
<script>mermaid.initialize({
|
|
15895
16146
|
startOnLoad: true,
|
|
@@ -16713,13 +16964,60 @@ tr:hover td {
|
|
|
16713
16964
|
white-space: nowrap;
|
|
16714
16965
|
}
|
|
16715
16966
|
|
|
16967
|
+
.gantt-grid-line {
|
|
16968
|
+
position: absolute;
|
|
16969
|
+
top: 0;
|
|
16970
|
+
bottom: 0;
|
|
16971
|
+
width: 1px;
|
|
16972
|
+
background: var(--border);
|
|
16973
|
+
opacity: 0.35;
|
|
16974
|
+
}
|
|
16975
|
+
|
|
16976
|
+
.gantt-sprint-line {
|
|
16977
|
+
position: absolute;
|
|
16978
|
+
top: 0;
|
|
16979
|
+
bottom: 0;
|
|
16980
|
+
width: 1px;
|
|
16981
|
+
background: var(--text-dim);
|
|
16982
|
+
opacity: 0.3;
|
|
16983
|
+
}
|
|
16984
|
+
|
|
16716
16985
|
.gantt-today {
|
|
16717
16986
|
position: absolute;
|
|
16718
16987
|
top: 0;
|
|
16719
16988
|
bottom: 0;
|
|
16720
|
-
width:
|
|
16989
|
+
width: 3px;
|
|
16721
16990
|
background: var(--red);
|
|
16722
|
-
opacity: 0.
|
|
16991
|
+
opacity: 0.8;
|
|
16992
|
+
border-radius: 1px;
|
|
16993
|
+
}
|
|
16994
|
+
|
|
16995
|
+
/* Sprint band in timeline */
|
|
16996
|
+
.gantt-sprint-band-row {
|
|
16997
|
+
border-bottom: 1px solid var(--border);
|
|
16998
|
+
margin-bottom: 0.25rem;
|
|
16999
|
+
}
|
|
17000
|
+
|
|
17001
|
+
.gantt-sprint-band {
|
|
17002
|
+
height: 32px;
|
|
17003
|
+
}
|
|
17004
|
+
|
|
17005
|
+
.gantt-sprint-block {
|
|
17006
|
+
position: absolute;
|
|
17007
|
+
top: 2px;
|
|
17008
|
+
bottom: 2px;
|
|
17009
|
+
background: var(--bg-hover);
|
|
17010
|
+
border: 1px solid var(--border);
|
|
17011
|
+
border-radius: 4px;
|
|
17012
|
+
font-size: 0.65rem;
|
|
17013
|
+
color: var(--text-dim);
|
|
17014
|
+
display: flex;
|
|
17015
|
+
align-items: center;
|
|
17016
|
+
justify-content: center;
|
|
17017
|
+
overflow: hidden;
|
|
17018
|
+
white-space: nowrap;
|
|
17019
|
+
text-overflow: ellipsis;
|
|
17020
|
+
padding: 0 0.4rem;
|
|
16723
17021
|
}
|
|
16724
17022
|
|
|
16725
17023
|
/* Pie chart color overrides */
|
|
@@ -16781,6 +17079,146 @@ tr:hover td {
|
|
|
16781
17079
|
}
|
|
16782
17080
|
|
|
16783
17081
|
.text-dim { color: var(--text-dim); }
|
|
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
|
+
|
|
17189
|
+
/* Collapsible sections */
|
|
17190
|
+
.collapsible-header {
|
|
17191
|
+
cursor: pointer;
|
|
17192
|
+
display: flex;
|
|
17193
|
+
align-items: center;
|
|
17194
|
+
gap: 0.4rem;
|
|
17195
|
+
user-select: none;
|
|
17196
|
+
}
|
|
17197
|
+
|
|
17198
|
+
.collapsible-header:hover {
|
|
17199
|
+
color: var(--accent);
|
|
17200
|
+
}
|
|
17201
|
+
|
|
17202
|
+
.collapsible-chevron {
|
|
17203
|
+
transition: transform 0.2s ease;
|
|
17204
|
+
flex-shrink: 0;
|
|
17205
|
+
}
|
|
17206
|
+
|
|
17207
|
+
.collapsible.collapsed .collapsible-chevron {
|
|
17208
|
+
transform: rotate(-90deg);
|
|
17209
|
+
}
|
|
17210
|
+
|
|
17211
|
+
.collapsible-body {
|
|
17212
|
+
overflow: hidden;
|
|
17213
|
+
max-height: 5000px;
|
|
17214
|
+
transition: max-height 0.3s ease, opacity 0.2s ease;
|
|
17215
|
+
opacity: 1;
|
|
17216
|
+
}
|
|
17217
|
+
|
|
17218
|
+
.collapsible.collapsed .collapsible-body {
|
|
17219
|
+
max-height: 0;
|
|
17220
|
+
opacity: 0;
|
|
17221
|
+
}
|
|
16784
17222
|
`;
|
|
16785
17223
|
}
|
|
16786
17224
|
|
|
@@ -16833,35 +17271,73 @@ function buildTimelineGantt(data, maxSprints = 6) {
|
|
|
16833
17271
|
);
|
|
16834
17272
|
tick += 7 * DAY;
|
|
16835
17273
|
}
|
|
17274
|
+
const gridLines = [];
|
|
17275
|
+
let gridTick = timelineStart;
|
|
17276
|
+
const gridStartDay = new Date(gridTick).getDay();
|
|
17277
|
+
gridTick += (8 - gridStartDay) % 7 * DAY;
|
|
17278
|
+
while (gridTick <= timelineEnd) {
|
|
17279
|
+
gridLines.push(`<div class="gantt-grid-line" style="left:${pct(gridTick).toFixed(2)}%"></div>`);
|
|
17280
|
+
gridTick += 7 * DAY;
|
|
17281
|
+
}
|
|
17282
|
+
const sprintBoundaries = /* @__PURE__ */ new Set();
|
|
17283
|
+
for (const sprint of visibleSprints) {
|
|
17284
|
+
sprintBoundaries.add(toMs(sprint.startDate));
|
|
17285
|
+
sprintBoundaries.add(toMs(sprint.endDate));
|
|
17286
|
+
}
|
|
17287
|
+
const sprintLines = [...sprintBoundaries].map(
|
|
17288
|
+
(ms) => `<div class="gantt-sprint-line" style="left:${pct(ms).toFixed(2)}%"></div>`
|
|
17289
|
+
);
|
|
16836
17290
|
const now = Date.now();
|
|
16837
17291
|
let todayMarker = "";
|
|
16838
17292
|
if (now >= timelineStart && now <= timelineEnd) {
|
|
16839
17293
|
todayMarker = `<div class="gantt-today" style="left:${pct(now).toFixed(2)}%"></div>`;
|
|
16840
17294
|
}
|
|
16841
|
-
const
|
|
17295
|
+
const sprintBlocks = visibleSprints.map((sprint) => {
|
|
17296
|
+
const sStart = toMs(sprint.startDate);
|
|
17297
|
+
const sEnd = toMs(sprint.endDate);
|
|
17298
|
+
const left = pct(sStart).toFixed(2);
|
|
17299
|
+
const width = (pct(sEnd) - pct(sStart)).toFixed(2);
|
|
17300
|
+
return `<div class="gantt-sprint-block" style="left:${left}%;width:${width}%">${sanitize(sprint.id, 20)}</div>`;
|
|
17301
|
+
}).join("");
|
|
17302
|
+
const sprintBandRow = `<div class="gantt-row gantt-sprint-band-row">
|
|
17303
|
+
<div class="gantt-label gantt-section-label">Sprints</div>
|
|
17304
|
+
<div class="gantt-track gantt-sprint-band">${sprintBlocks}</div>
|
|
17305
|
+
</div>`;
|
|
17306
|
+
const epicSpanMap = /* @__PURE__ */ new Map();
|
|
16842
17307
|
for (const sprint of visibleSprints) {
|
|
16843
17308
|
const sStart = toMs(sprint.startDate);
|
|
16844
17309
|
const sEnd = toMs(sprint.endDate);
|
|
16845
|
-
|
|
16846
|
-
|
|
16847
|
-
|
|
16848
|
-
|
|
16849
|
-
|
|
16850
|
-
|
|
16851
|
-
|
|
16852
|
-
|
|
16853
|
-
|
|
16854
|
-
|
|
16855
|
-
|
|
16856
|
-
|
|
16857
|
-
|
|
16858
|
-
|
|
17310
|
+
for (const eid of sprint.linkedEpics) {
|
|
17311
|
+
if (!epicMap.has(eid)) continue;
|
|
17312
|
+
const existing = epicSpanMap.get(eid);
|
|
17313
|
+
if (existing) {
|
|
17314
|
+
existing.startMs = Math.min(existing.startMs, sStart);
|
|
17315
|
+
existing.endMs = Math.max(existing.endMs, sEnd);
|
|
17316
|
+
} else {
|
|
17317
|
+
epicSpanMap.set(eid, { startMs: sStart, endMs: sEnd });
|
|
17318
|
+
}
|
|
17319
|
+
}
|
|
17320
|
+
}
|
|
17321
|
+
const sortedEpicIds = [...epicSpanMap.keys()].sort((a, b) => {
|
|
17322
|
+
const aSpan = epicSpanMap.get(a);
|
|
17323
|
+
const bSpan = epicSpanMap.get(b);
|
|
17324
|
+
if (aSpan.startMs !== bSpan.startMs) return aSpan.startMs - bSpan.startMs;
|
|
17325
|
+
return a.localeCompare(b);
|
|
17326
|
+
});
|
|
17327
|
+
const epicRows = sortedEpicIds.map((eid) => {
|
|
17328
|
+
const epic = epicMap.get(eid);
|
|
17329
|
+
const { startMs, endMs } = epicSpanMap.get(eid);
|
|
17330
|
+
const cls = epic.status === "done" || epic.status === "completed" ? "gantt-bar-done" : epic.status === "in-progress" || epic.status === "active" ? "gantt-bar-active" : epic.status === "blocked" ? "gantt-bar-blocked" : "gantt-bar-default";
|
|
17331
|
+
const left = pct(startMs).toFixed(2);
|
|
17332
|
+
const width = (pct(endMs) - pct(startMs)).toFixed(2);
|
|
17333
|
+
const label = sanitize(epic.id + " " + epic.title);
|
|
17334
|
+
return `<div class="gantt-row">
|
|
17335
|
+
<div class="gantt-label">${label}</div>
|
|
16859
17336
|
<div class="gantt-track">
|
|
16860
17337
|
<div class="gantt-bar ${cls}" style="left:${left}%;width:${width}%"></div>
|
|
16861
17338
|
</div>
|
|
16862
|
-
</div
|
|
16863
|
-
|
|
16864
|
-
}
|
|
17339
|
+
</div>`;
|
|
17340
|
+
}).join("\n");
|
|
16865
17341
|
const note = truncated ? `<div class="mermaid-note">${hiddenCount} earlier sprint${hiddenCount > 1 ? "s" : ""} not shown</div>` : "";
|
|
16866
17342
|
return `${note}
|
|
16867
17343
|
<div class="gantt">
|
|
@@ -16870,11 +17346,12 @@ function buildTimelineGantt(data, maxSprints = 6) {
|
|
|
16870
17346
|
<div class="gantt-label"></div>
|
|
16871
17347
|
<div class="gantt-track gantt-dates">${markers.join("")}</div>
|
|
16872
17348
|
</div>
|
|
16873
|
-
${
|
|
17349
|
+
${sprintBandRow}
|
|
17350
|
+
${epicRows}
|
|
16874
17351
|
</div>
|
|
16875
17352
|
<div class="gantt-overlay">
|
|
16876
17353
|
<div class="gantt-label"></div>
|
|
16877
|
-
<div class="gantt-track">${todayMarker}</div>
|
|
17354
|
+
<div class="gantt-track">${gridLines.join("")}${sprintLines.join("")}${todayMarker}</div>
|
|
16878
17355
|
</div>
|
|
16879
17356
|
</div>`;
|
|
16880
17357
|
}
|
|
@@ -17154,11 +17631,12 @@ function overviewPage(data, diagrams, navGroups) {
|
|
|
17154
17631
|
|
|
17155
17632
|
<div class="section-title"><a href="/timeline">Project Timeline →</a></div>
|
|
17156
17633
|
|
|
17157
|
-
|
|
17158
|
-
${buildArtifactFlowchart(diagrams)}
|
|
17634
|
+
${collapsibleSection("overview-relationships", "Artifact Relationships", buildArtifactFlowchart(diagrams))}
|
|
17159
17635
|
|
|
17160
|
-
|
|
17161
|
-
|
|
17636
|
+
${collapsibleSection(
|
|
17637
|
+
"overview-recent",
|
|
17638
|
+
"Recent Activity",
|
|
17639
|
+
data.recent.length > 0 ? `
|
|
17162
17640
|
<div class="table-wrap">
|
|
17163
17641
|
<table>
|
|
17164
17642
|
<thead>
|
|
@@ -17174,7 +17652,8 @@ function overviewPage(data, diagrams, navGroups) {
|
|
|
17174
17652
|
${rows}
|
|
17175
17653
|
</tbody>
|
|
17176
17654
|
</table>
|
|
17177
|
-
</div>` : `<div class="empty"><p>No documents yet.</p></div>`
|
|
17655
|
+
</div>` : `<div class="empty"><p>No documents yet.</p></div>`
|
|
17656
|
+
)}
|
|
17178
17657
|
`;
|
|
17179
17658
|
}
|
|
17180
17659
|
|
|
@@ -17319,23 +17798,24 @@ function garPage(report) {
|
|
|
17319
17798
|
<div class="label">Overall: ${escapeHtml(report.overall)}</div>
|
|
17320
17799
|
</div>
|
|
17321
17800
|
|
|
17322
|
-
|
|
17323
|
-
${areaCards}
|
|
17324
|
-
</div>
|
|
17801
|
+
${collapsibleSection("gar-areas", "Areas", `<div class="gar-areas">${areaCards}</div>`)}
|
|
17325
17802
|
|
|
17326
|
-
|
|
17327
|
-
|
|
17328
|
-
|
|
17329
|
-
|
|
17330
|
-
|
|
17331
|
-
|
|
17803
|
+
${collapsibleSection(
|
|
17804
|
+
"gar-status-dist",
|
|
17805
|
+
"Status Distribution",
|
|
17806
|
+
buildStatusPie("Action Status", {
|
|
17807
|
+
Open: report.metrics.scope.open,
|
|
17808
|
+
Done: report.metrics.scope.done,
|
|
17809
|
+
"In Progress": Math.max(0, report.metrics.scope.total - report.metrics.scope.open - report.metrics.scope.done)
|
|
17810
|
+
})
|
|
17811
|
+
)}
|
|
17332
17812
|
`;
|
|
17333
17813
|
}
|
|
17334
17814
|
|
|
17335
17815
|
// src/web/templates/pages/health.ts
|
|
17336
17816
|
function healthPage(report, metrics) {
|
|
17337
17817
|
const dotClass = `dot-${report.overall}`;
|
|
17338
|
-
function renderSection(title, categories) {
|
|
17818
|
+
function renderSection(sectionId, title, categories) {
|
|
17339
17819
|
const cards = categories.map(
|
|
17340
17820
|
(cat) => `
|
|
17341
17821
|
<div class="gar-area">
|
|
@@ -17347,10 +17827,9 @@ function healthPage(report, metrics) {
|
|
|
17347
17827
|
${cat.items.length > 0 ? `<ul>${cat.items.map((item) => `<li><span class="ref-id">${escapeHtml(item.id)}</span>${escapeHtml(item.detail)}</li>`).join("")}</ul>` : ""}
|
|
17348
17828
|
</div>`
|
|
17349
17829
|
).join("\n");
|
|
17350
|
-
return
|
|
17351
|
-
|
|
17352
|
-
|
|
17353
|
-
`;
|
|
17830
|
+
return collapsibleSection(sectionId, title, `<div class="gar-areas">${cards}</div>`, {
|
|
17831
|
+
titleClass: "health-section-title"
|
|
17832
|
+
});
|
|
17354
17833
|
}
|
|
17355
17834
|
return `
|
|
17356
17835
|
<div class="page-header">
|
|
@@ -17363,35 +17842,43 @@ function healthPage(report, metrics) {
|
|
|
17363
17842
|
<div class="label">Overall: ${escapeHtml(report.overall)}</div>
|
|
17364
17843
|
</div>
|
|
17365
17844
|
|
|
17366
|
-
${renderSection("Completeness", report.completeness)}
|
|
17367
|
-
|
|
17368
|
-
|
|
17369
|
-
|
|
17370
|
-
|
|
17371
|
-
|
|
17372
|
-
|
|
17373
|
-
|
|
17374
|
-
|
|
17375
|
-
|
|
17376
|
-
|
|
17377
|
-
|
|
17378
|
-
|
|
17379
|
-
|
|
17380
|
-
|
|
17381
|
-
|
|
17845
|
+
${renderSection("health-completeness", "Completeness", report.completeness)}
|
|
17846
|
+
|
|
17847
|
+
${collapsibleSection(
|
|
17848
|
+
"health-completeness-overview",
|
|
17849
|
+
"Completeness Overview",
|
|
17850
|
+
buildHealthGauge(
|
|
17851
|
+
metrics ? Object.entries(metrics.completeness).map(([name, cat]) => ({
|
|
17852
|
+
name: name.replace(/\b\w/g, (c) => c.toUpperCase()),
|
|
17853
|
+
complete: cat.complete,
|
|
17854
|
+
total: cat.total
|
|
17855
|
+
})) : report.completeness.map((c) => {
|
|
17856
|
+
const match = c.summary.match(/(\d+)\s*\/\s*(\d+)/);
|
|
17857
|
+
return {
|
|
17858
|
+
name: c.name,
|
|
17859
|
+
complete: match ? parseInt(match[1], 10) : 0,
|
|
17860
|
+
total: match ? parseInt(match[2], 10) : 0
|
|
17861
|
+
};
|
|
17862
|
+
})
|
|
17863
|
+
),
|
|
17864
|
+
{ titleClass: "health-section-title" }
|
|
17382
17865
|
)}
|
|
17383
17866
|
|
|
17384
|
-
${renderSection("Process", report.process)}
|
|
17385
|
-
|
|
17386
|
-
|
|
17387
|
-
|
|
17388
|
-
|
|
17389
|
-
"
|
|
17390
|
-
|
|
17391
|
-
|
|
17392
|
-
|
|
17393
|
-
|
|
17394
|
-
|
|
17867
|
+
${renderSection("health-process", "Process", report.process)}
|
|
17868
|
+
|
|
17869
|
+
${collapsibleSection(
|
|
17870
|
+
"health-process-summary",
|
|
17871
|
+
"Process Summary",
|
|
17872
|
+
metrics ? buildStatusPie("Process Health", {
|
|
17873
|
+
Stale: metrics.process.stale.length,
|
|
17874
|
+
"Aging Actions": metrics.process.agingActions.length,
|
|
17875
|
+
Healthy: Math.max(
|
|
17876
|
+
0,
|
|
17877
|
+
(metrics.completeness ? Object.values(metrics.completeness).reduce((sum, c) => sum + c.total, 0) : 0) - metrics.process.stale.length - metrics.process.agingActions.length
|
|
17878
|
+
)
|
|
17879
|
+
}) : "",
|
|
17880
|
+
{ titleClass: "health-section-title" }
|
|
17881
|
+
)}
|
|
17395
17882
|
`;
|
|
17396
17883
|
}
|
|
17397
17884
|
|
|
@@ -17449,7 +17936,7 @@ function timelinePage(diagrams) {
|
|
|
17449
17936
|
return `
|
|
17450
17937
|
<div class="page-header">
|
|
17451
17938
|
<h2>Project Timeline</h2>
|
|
17452
|
-
<div class="subtitle">
|
|
17939
|
+
<div class="subtitle">Epic timeline across sprints</div>
|
|
17453
17940
|
</div>
|
|
17454
17941
|
|
|
17455
17942
|
${buildTimelineGantt(diagrams)}
|
|
@@ -17477,9 +17964,10 @@ function upcomingPage(data) {
|
|
|
17477
17964
|
const hasActions = data.dueSoonActions.length > 0;
|
|
17478
17965
|
const hasSprintTasks = data.dueSoonSprintTasks.length > 0;
|
|
17479
17966
|
const hasTrending = data.trending.length > 0;
|
|
17480
|
-
const actionsTable = hasActions ?
|
|
17481
|
-
|
|
17482
|
-
|
|
17967
|
+
const actionsTable = hasActions ? collapsibleSection(
|
|
17968
|
+
"upcoming-actions",
|
|
17969
|
+
"Due Soon \u2014 Actions",
|
|
17970
|
+
`<div class="table-wrap">
|
|
17483
17971
|
<table>
|
|
17484
17972
|
<thead>
|
|
17485
17973
|
<tr>
|
|
@@ -17494,7 +17982,7 @@ function upcomingPage(data) {
|
|
|
17494
17982
|
</thead>
|
|
17495
17983
|
<tbody>
|
|
17496
17984
|
${data.dueSoonActions.map(
|
|
17497
|
-
|
|
17985
|
+
(a) => `
|
|
17498
17986
|
<tr class="${urgencyRowClass(a.urgency)}">
|
|
17499
17987
|
<td><a href="/docs/action/${escapeHtml(a.id)}">${escapeHtml(a.id)}</a></td>
|
|
17500
17988
|
<td>${escapeHtml(a.title)}</td>
|
|
@@ -17504,13 +17992,16 @@ function upcomingPage(data) {
|
|
|
17504
17992
|
<td>${urgencyBadge(a.urgency)}</td>
|
|
17505
17993
|
<td>${a.relatedTaskCount > 0 ? a.relatedTaskCount : "\u2014"}</td>
|
|
17506
17994
|
</tr>`
|
|
17507
|
-
|
|
17995
|
+
).join("")}
|
|
17508
17996
|
</tbody>
|
|
17509
17997
|
</table>
|
|
17510
|
-
</div
|
|
17511
|
-
|
|
17512
|
-
|
|
17513
|
-
|
|
17998
|
+
</div>`,
|
|
17999
|
+
{ titleTag: "h3" }
|
|
18000
|
+
) : "";
|
|
18001
|
+
const sprintTasksTable = hasSprintTasks ? collapsibleSection(
|
|
18002
|
+
"upcoming-sprint-tasks",
|
|
18003
|
+
"Due Soon \u2014 Sprint Tasks",
|
|
18004
|
+
`<div class="table-wrap">
|
|
17514
18005
|
<table>
|
|
17515
18006
|
<thead>
|
|
17516
18007
|
<tr>
|
|
@@ -17524,7 +18015,7 @@ function upcomingPage(data) {
|
|
|
17524
18015
|
</thead>
|
|
17525
18016
|
<tbody>
|
|
17526
18017
|
${data.dueSoonSprintTasks.map(
|
|
17527
|
-
|
|
18018
|
+
(t) => `
|
|
17528
18019
|
<tr class="${urgencyRowClass(t.urgency)}">
|
|
17529
18020
|
<td><a href="/docs/task/${escapeHtml(t.id)}">${escapeHtml(t.id)}</a></td>
|
|
17530
18021
|
<td>${escapeHtml(t.title)}</td>
|
|
@@ -17533,13 +18024,16 @@ function upcomingPage(data) {
|
|
|
17533
18024
|
<td>${formatDate(t.sprintEndDate)}</td>
|
|
17534
18025
|
<td>${urgencyBadge(t.urgency)}</td>
|
|
17535
18026
|
</tr>`
|
|
17536
|
-
|
|
18027
|
+
).join("")}
|
|
17537
18028
|
</tbody>
|
|
17538
18029
|
</table>
|
|
17539
|
-
</div
|
|
17540
|
-
|
|
17541
|
-
|
|
17542
|
-
|
|
18030
|
+
</div>`,
|
|
18031
|
+
{ titleTag: "h3" }
|
|
18032
|
+
) : "";
|
|
18033
|
+
const trendingTable = hasTrending ? collapsibleSection(
|
|
18034
|
+
"upcoming-trending",
|
|
18035
|
+
"Trending",
|
|
18036
|
+
`<div class="table-wrap">
|
|
17543
18037
|
<table>
|
|
17544
18038
|
<thead>
|
|
17545
18039
|
<tr>
|
|
@@ -17554,7 +18048,7 @@ function upcomingPage(data) {
|
|
|
17554
18048
|
</thead>
|
|
17555
18049
|
<tbody>
|
|
17556
18050
|
${data.trending.map(
|
|
17557
|
-
|
|
18051
|
+
(t, i) => `
|
|
17558
18052
|
<tr>
|
|
17559
18053
|
<td><span class="trending-rank">${i + 1}</span></td>
|
|
17560
18054
|
<td><a href="/docs/${escapeHtml(t.type)}/${escapeHtml(t.id)}">${escapeHtml(t.id)}</a></td>
|
|
@@ -17564,10 +18058,12 @@ function upcomingPage(data) {
|
|
|
17564
18058
|
<td><span class="trending-score">${t.score}</span></td>
|
|
17565
18059
|
<td>${t.signals.map((s) => `<span class="signal-tag">${escapeHtml(s.factor)} +${s.points}</span>`).join(" ")}</td>
|
|
17566
18060
|
</tr>`
|
|
17567
|
-
|
|
18061
|
+
).join("")}
|
|
17568
18062
|
</tbody>
|
|
17569
18063
|
</table>
|
|
17570
|
-
</div
|
|
18064
|
+
</div>`,
|
|
18065
|
+
{ titleTag: "h3" }
|
|
18066
|
+
) : "";
|
|
17571
18067
|
const emptyState = !hasActions && !hasSprintTasks && !hasTrending ? '<div class="empty"><p>No upcoming items or trending activity found.</p></div>' : "";
|
|
17572
18068
|
return `
|
|
17573
18069
|
<div class="page-header">
|
|
@@ -17581,7 +18077,317 @@ function upcomingPage(data) {
|
|
|
17581
18077
|
`;
|
|
17582
18078
|
}
|
|
17583
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
|
+
|
|
17584
18389
|
// src/web/router.ts
|
|
18390
|
+
var sprintSummaryCache = /* @__PURE__ */ new Map();
|
|
17585
18391
|
function handleRequest(req, res, store, projectName, navGroups) {
|
|
17586
18392
|
const parsed = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
17587
18393
|
const pathname = parsed.pathname;
|
|
@@ -17627,6 +18433,42 @@ function handleRequest(req, res, store, projectName, navGroups) {
|
|
|
17627
18433
|
respond(res, layout({ title: "Upcoming", activePath: "/upcoming", projectName, navGroups }, body));
|
|
17628
18434
|
return;
|
|
17629
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
|
+
}
|
|
17630
18472
|
const boardMatch = pathname.match(/^\/board(?:\/([^/]+))?$/);
|
|
17631
18473
|
if (boardMatch) {
|
|
17632
18474
|
const type = boardMatch[1];
|
|
@@ -18156,6 +18998,25 @@ function createReportTools(store) {
|
|
|
18156
18998
|
},
|
|
18157
18999
|
{ annotations: { readOnlyHint: true } }
|
|
18158
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
|
+
),
|
|
18159
19020
|
tool8(
|
|
18160
19021
|
"save_report",
|
|
18161
19022
|
"Save a generated report as a persistent document",
|
|
@@ -20865,8 +21726,8 @@ function gatherContext(store, focusFeature, includeDecisions = true, includeQues
|
|
|
20865
21726
|
title: e.frontmatter.title,
|
|
20866
21727
|
status: e.frontmatter.status,
|
|
20867
21728
|
linkedFeature: normalizeLinkedFeatures(e.frontmatter.linkedFeature),
|
|
20868
|
-
targetDate: e.frontmatter.targetDate
|
|
20869
|
-
estimatedEffort: e.frontmatter.estimatedEffort
|
|
21729
|
+
targetDate: typeof e.frontmatter.targetDate === "string" ? e.frontmatter.targetDate : null,
|
|
21730
|
+
estimatedEffort: typeof e.frontmatter.estimatedEffort === "string" ? e.frontmatter.estimatedEffort : null,
|
|
20870
21731
|
content: e.content,
|
|
20871
21732
|
linkedTaskCount: tasks.filter(
|
|
20872
21733
|
(t) => normalizeLinkedEpics(t.frontmatter.linkedEpic).includes(e.frontmatter.id)
|
|
@@ -20877,10 +21738,10 @@ function gatherContext(store, focusFeature, includeDecisions = true, includeQues
|
|
|
20877
21738
|
title: t.frontmatter.title,
|
|
20878
21739
|
status: t.frontmatter.status,
|
|
20879
21740
|
linkedEpic: normalizeLinkedEpics(t.frontmatter.linkedEpic),
|
|
20880
|
-
acceptanceCriteria: t.frontmatter.acceptanceCriteria
|
|
20881
|
-
technicalNotes: t.frontmatter.technicalNotes
|
|
20882
|
-
complexity: t.frontmatter.complexity
|
|
20883
|
-
estimatedPoints: t.frontmatter.estimatedPoints
|
|
21741
|
+
acceptanceCriteria: typeof t.frontmatter.acceptanceCriteria === "string" ? t.frontmatter.acceptanceCriteria : null,
|
|
21742
|
+
technicalNotes: typeof t.frontmatter.technicalNotes === "string" ? t.frontmatter.technicalNotes : null,
|
|
21743
|
+
complexity: typeof t.frontmatter.complexity === "string" ? t.frontmatter.complexity : null,
|
|
21744
|
+
estimatedPoints: typeof t.frontmatter.estimatedPoints === "number" ? t.frontmatter.estimatedPoints : null,
|
|
20884
21745
|
priority: t.frontmatter.priority ?? null
|
|
20885
21746
|
})),
|
|
20886
21747
|
decisions: allDecisions.map((d) => ({
|
|
@@ -21703,6 +22564,24 @@ function createWebTools(store, projectName, navGroups) {
|
|
|
21703
22564
|
};
|
|
21704
22565
|
},
|
|
21705
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 } }
|
|
21706
22585
|
)
|
|
21707
22586
|
];
|
|
21708
22587
|
}
|
|
@@ -21734,7 +22613,7 @@ import * as readline from "readline";
|
|
|
21734
22613
|
import chalk from "chalk";
|
|
21735
22614
|
import ora from "ora";
|
|
21736
22615
|
import {
|
|
21737
|
-
query as
|
|
22616
|
+
query as query3
|
|
21738
22617
|
} from "@anthropic-ai/claude-agent-sdk";
|
|
21739
22618
|
|
|
21740
22619
|
// src/storage/session-store.ts
|
|
@@ -21805,11 +22684,11 @@ var SessionStore = class {
|
|
|
21805
22684
|
};
|
|
21806
22685
|
|
|
21807
22686
|
// src/agent/session-namer.ts
|
|
21808
|
-
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
22687
|
+
import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
|
|
21809
22688
|
async function generateSessionName(turns) {
|
|
21810
22689
|
try {
|
|
21811
22690
|
const transcript = turns.slice(-20).map((t) => `${t.role}: ${t.content.slice(0, 200)}`).join("\n");
|
|
21812
|
-
const result =
|
|
22691
|
+
const result = query2({
|
|
21813
22692
|
prompt: `Summarize this conversation in 3-5 words as a kebab-case name suitable for a filename. Output ONLY the name, nothing else.
|
|
21814
22693
|
|
|
21815
22694
|
${transcript}`,
|
|
@@ -22076,6 +22955,7 @@ Marvin \u2014 ${persona.name}
|
|
|
22076
22955
|
"mcp__marvin-governance__get_dashboard_gar",
|
|
22077
22956
|
"mcp__marvin-governance__get_dashboard_board",
|
|
22078
22957
|
"mcp__marvin-governance__get_dashboard_upcoming",
|
|
22958
|
+
"mcp__marvin-governance__get_dashboard_sprint_summary",
|
|
22079
22959
|
...pluginTools.map((t) => `mcp__marvin-governance__${t.name}`),
|
|
22080
22960
|
...codeSkillTools.map((t) => `mcp__marvin-governance__${t.name}`)
|
|
22081
22961
|
]
|
|
@@ -22086,7 +22966,7 @@ Marvin \u2014 ${persona.name}
|
|
|
22086
22966
|
if (existingSession) {
|
|
22087
22967
|
queryOptions.resume = existingSession.id;
|
|
22088
22968
|
}
|
|
22089
|
-
const conversation =
|
|
22969
|
+
const conversation = query3({
|
|
22090
22970
|
prompt,
|
|
22091
22971
|
options: queryOptions
|
|
22092
22972
|
});
|
|
@@ -22178,7 +23058,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
22178
23058
|
import { tool as tool23 } from "@anthropic-ai/claude-agent-sdk";
|
|
22179
23059
|
|
|
22180
23060
|
// src/skills/action-runner.ts
|
|
22181
|
-
import { query as
|
|
23061
|
+
import { query as query4 } from "@anthropic-ai/claude-agent-sdk";
|
|
22182
23062
|
var GOVERNANCE_TOOL_NAMES2 = [
|
|
22183
23063
|
"mcp__marvin-governance__list_decisions",
|
|
22184
23064
|
"mcp__marvin-governance__get_decision",
|
|
@@ -22200,7 +23080,7 @@ async function runSkillAction(action, userPrompt, context) {
|
|
|
22200
23080
|
try {
|
|
22201
23081
|
const mcpServer = createMarvinMcpServer(context.store);
|
|
22202
23082
|
const allowedTools = action.allowGovernanceTools !== false ? GOVERNANCE_TOOL_NAMES2 : [];
|
|
22203
|
-
const conversation =
|
|
23083
|
+
const conversation = query4({
|
|
22204
23084
|
prompt: userPrompt,
|
|
22205
23085
|
options: {
|
|
22206
23086
|
systemPrompt: action.systemPrompt,
|
|
@@ -22994,7 +23874,7 @@ import * as fs13 from "fs";
|
|
|
22994
23874
|
import * as path13 from "path";
|
|
22995
23875
|
import chalk7 from "chalk";
|
|
22996
23876
|
import ora2 from "ora";
|
|
22997
|
-
import { query as
|
|
23877
|
+
import { query as query5 } from "@anthropic-ai/claude-agent-sdk";
|
|
22998
23878
|
|
|
22999
23879
|
// src/sources/prompts.ts
|
|
23000
23880
|
function buildIngestSystemPrompt(persona, projectConfig, isDraft) {
|
|
@@ -23127,7 +24007,7 @@ async function ingestFile(options) {
|
|
|
23127
24007
|
const spinner = ora2({ text: `Analyzing ${fileName}...`, color: "cyan" });
|
|
23128
24008
|
spinner.start();
|
|
23129
24009
|
try {
|
|
23130
|
-
const conversation =
|
|
24010
|
+
const conversation = query5({
|
|
23131
24011
|
prompt: userPrompt,
|
|
23132
24012
|
options: {
|
|
23133
24013
|
systemPrompt,
|
|
@@ -24348,7 +25228,7 @@ import chalk13 from "chalk";
|
|
|
24348
25228
|
// src/analysis/analyze.ts
|
|
24349
25229
|
import chalk12 from "chalk";
|
|
24350
25230
|
import ora4 from "ora";
|
|
24351
|
-
import { query as
|
|
25231
|
+
import { query as query6 } from "@anthropic-ai/claude-agent-sdk";
|
|
24352
25232
|
|
|
24353
25233
|
// src/analysis/prompts.ts
|
|
24354
25234
|
function buildAnalyzeSystemPrompt(persona, projectConfig, isDraft) {
|
|
@@ -24478,7 +25358,7 @@ async function analyzeMeeting(options) {
|
|
|
24478
25358
|
const spinner = ora4({ text: `Analyzing meeting ${meetingId}...`, color: "cyan" });
|
|
24479
25359
|
spinner.start();
|
|
24480
25360
|
try {
|
|
24481
|
-
const conversation =
|
|
25361
|
+
const conversation = query6({
|
|
24482
25362
|
prompt: userPrompt,
|
|
24483
25363
|
options: {
|
|
24484
25364
|
systemPrompt,
|
|
@@ -24605,7 +25485,7 @@ import chalk15 from "chalk";
|
|
|
24605
25485
|
// src/contributions/contribute.ts
|
|
24606
25486
|
import chalk14 from "chalk";
|
|
24607
25487
|
import ora5 from "ora";
|
|
24608
|
-
import { query as
|
|
25488
|
+
import { query as query7 } from "@anthropic-ai/claude-agent-sdk";
|
|
24609
25489
|
|
|
24610
25490
|
// src/contributions/prompts.ts
|
|
24611
25491
|
function buildContributeSystemPrompt(persona, contributionType, projectConfig, isDraft) {
|
|
@@ -24859,7 +25739,7 @@ async function contributeFromPersona(options) {
|
|
|
24859
25739
|
"mcp__marvin-governance__get_action",
|
|
24860
25740
|
"mcp__marvin-governance__get_question"
|
|
24861
25741
|
];
|
|
24862
|
-
const conversation =
|
|
25742
|
+
const conversation = query7({
|
|
24863
25743
|
prompt: userPrompt,
|
|
24864
25744
|
options: {
|
|
24865
25745
|
systemPrompt,
|
|
@@ -25005,6 +25885,9 @@ Contribution: ${options.type}`));
|
|
|
25005
25885
|
});
|
|
25006
25886
|
}
|
|
25007
25887
|
|
|
25888
|
+
// src/cli/commands/report.ts
|
|
25889
|
+
import ora6 from "ora";
|
|
25890
|
+
|
|
25008
25891
|
// src/reports/gar/render-ascii.ts
|
|
25009
25892
|
import chalk16 from "chalk";
|
|
25010
25893
|
var STATUS_DOT = {
|
|
@@ -25199,6 +26082,47 @@ async function healthReportCommand(options) {
|
|
|
25199
26082
|
console.log(renderAscii2(report));
|
|
25200
26083
|
}
|
|
25201
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
|
+
}
|
|
25202
26126
|
|
|
25203
26127
|
// src/cli/commands/web.ts
|
|
25204
26128
|
async function webCommand(options) {
|
|
@@ -25241,7 +26165,7 @@ function createProgram() {
|
|
|
25241
26165
|
const program = new Command();
|
|
25242
26166
|
program.name("marvin").description(
|
|
25243
26167
|
"AI-powered product development assistant with Product Owner, Delivery Manager, and Technical Lead personas"
|
|
25244
|
-
).version("0.4.
|
|
26168
|
+
).version("0.4.7");
|
|
25245
26169
|
program.command("init").description("Initialize a new Marvin project in the current directory").action(async () => {
|
|
25246
26170
|
await initCommand();
|
|
25247
26171
|
});
|
|
@@ -25324,6 +26248,9 @@ function createProgram() {
|
|
|
25324
26248
|
).action(async (options) => {
|
|
25325
26249
|
await healthReportCommand(options);
|
|
25326
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
|
+
});
|
|
25327
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) => {
|
|
25328
26255
|
await webCommand(options);
|
|
25329
26256
|
});
|