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/marvin-serve.js
CHANGED
|
@@ -176,9 +176,9 @@ var DocumentStore = class {
|
|
|
176
176
|
}
|
|
177
177
|
}
|
|
178
178
|
}
|
|
179
|
-
list(
|
|
179
|
+
list(query3) {
|
|
180
180
|
const results = [];
|
|
181
|
-
const types =
|
|
181
|
+
const types = query3?.type ? [query3.type] : Object.keys(this.typeDirs);
|
|
182
182
|
for (const type of types) {
|
|
183
183
|
const dirName = this.typeDirs[type];
|
|
184
184
|
if (!dirName) continue;
|
|
@@ -189,9 +189,9 @@ var DocumentStore = class {
|
|
|
189
189
|
const filePath = path3.join(dir, file2);
|
|
190
190
|
const raw = fs3.readFileSync(filePath, "utf-8");
|
|
191
191
|
const doc = parseDocument(raw, filePath);
|
|
192
|
-
if (
|
|
193
|
-
if (
|
|
194
|
-
if (
|
|
192
|
+
if (query3?.status && doc.frontmatter.status !== query3.status) continue;
|
|
193
|
+
if (query3?.owner && doc.frontmatter.owner !== query3.owner) continue;
|
|
194
|
+
if (query3?.tag && (!doc.frontmatter.tags || !doc.frontmatter.tags.includes(query3.tag)))
|
|
195
195
|
continue;
|
|
196
196
|
results.push(doc);
|
|
197
197
|
}
|
|
@@ -15482,6 +15482,346 @@ function evaluateHealth(projectName, metrics) {
|
|
|
15482
15482
|
};
|
|
15483
15483
|
}
|
|
15484
15484
|
|
|
15485
|
+
// src/plugins/builtin/tools/task-utils.ts
|
|
15486
|
+
function normalizeLinkedEpics(value) {
|
|
15487
|
+
if (value === void 0 || value === null) return [];
|
|
15488
|
+
if (typeof value === "string") {
|
|
15489
|
+
try {
|
|
15490
|
+
const parsed = JSON.parse(value);
|
|
15491
|
+
if (Array.isArray(parsed)) return parsed.filter((v) => typeof v === "string");
|
|
15492
|
+
} catch {
|
|
15493
|
+
}
|
|
15494
|
+
return [value];
|
|
15495
|
+
}
|
|
15496
|
+
if (Array.isArray(value)) return value.filter((v) => typeof v === "string");
|
|
15497
|
+
return [];
|
|
15498
|
+
}
|
|
15499
|
+
function generateEpicTags(epics) {
|
|
15500
|
+
return epics.map((id) => `epic:${id}`);
|
|
15501
|
+
}
|
|
15502
|
+
|
|
15503
|
+
// src/reports/sprint-summary/collector.ts
|
|
15504
|
+
var DONE_STATUSES = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
15505
|
+
function collectSprintSummaryData(store, sprintId) {
|
|
15506
|
+
const allDocs = store.list();
|
|
15507
|
+
const sprintDocs = allDocs.filter((d) => d.frontmatter.type === "sprint");
|
|
15508
|
+
let sprintDoc;
|
|
15509
|
+
if (sprintId) {
|
|
15510
|
+
sprintDoc = sprintDocs.find((d) => d.frontmatter.id === sprintId);
|
|
15511
|
+
} else {
|
|
15512
|
+
sprintDoc = sprintDocs.find((d) => d.frontmatter.status === "active");
|
|
15513
|
+
}
|
|
15514
|
+
if (!sprintDoc) return null;
|
|
15515
|
+
const fm = sprintDoc.frontmatter;
|
|
15516
|
+
const startDate = fm.startDate;
|
|
15517
|
+
const endDate = fm.endDate;
|
|
15518
|
+
const today = /* @__PURE__ */ new Date();
|
|
15519
|
+
const todayStr = today.toISOString().slice(0, 10);
|
|
15520
|
+
let daysElapsed = 0;
|
|
15521
|
+
let daysRemaining = 0;
|
|
15522
|
+
let totalDays = 0;
|
|
15523
|
+
let percentComplete = 0;
|
|
15524
|
+
if (startDate && endDate) {
|
|
15525
|
+
const startMs = new Date(startDate).getTime();
|
|
15526
|
+
const endMs = new Date(endDate).getTime();
|
|
15527
|
+
const todayMs = today.getTime();
|
|
15528
|
+
const msPerDay = 864e5;
|
|
15529
|
+
totalDays = Math.max(1, Math.round((endMs - startMs) / msPerDay));
|
|
15530
|
+
daysElapsed = Math.max(0, Math.round((todayMs - startMs) / msPerDay));
|
|
15531
|
+
daysRemaining = Math.max(0, Math.round((endMs - todayMs) / msPerDay));
|
|
15532
|
+
percentComplete = Math.min(100, Math.round(daysElapsed / totalDays * 100));
|
|
15533
|
+
}
|
|
15534
|
+
const linkedEpicIds = normalizeLinkedEpics(fm.linkedEpics);
|
|
15535
|
+
const epicToTasks = /* @__PURE__ */ new Map();
|
|
15536
|
+
const allTasks = allDocs.filter((d) => d.frontmatter.type === "task");
|
|
15537
|
+
for (const task of allTasks) {
|
|
15538
|
+
const tags = task.frontmatter.tags ?? [];
|
|
15539
|
+
for (const tag of tags) {
|
|
15540
|
+
if (tag.startsWith("epic:")) {
|
|
15541
|
+
const epicId = tag.slice(5);
|
|
15542
|
+
if (!epicToTasks.has(epicId)) epicToTasks.set(epicId, []);
|
|
15543
|
+
epicToTasks.get(epicId).push(task);
|
|
15544
|
+
}
|
|
15545
|
+
}
|
|
15546
|
+
}
|
|
15547
|
+
const linkedEpics = linkedEpicIds.map((epicId) => {
|
|
15548
|
+
const epic = store.get(epicId);
|
|
15549
|
+
const tasks = epicToTasks.get(epicId) ?? [];
|
|
15550
|
+
const tasksDone = tasks.filter((t) => DONE_STATUSES.has(t.frontmatter.status)).length;
|
|
15551
|
+
return {
|
|
15552
|
+
id: epicId,
|
|
15553
|
+
title: epic?.frontmatter.title ?? "(not found)",
|
|
15554
|
+
status: epic?.frontmatter.status ?? "unknown",
|
|
15555
|
+
tasksDone,
|
|
15556
|
+
tasksTotal: tasks.length
|
|
15557
|
+
};
|
|
15558
|
+
});
|
|
15559
|
+
const sprintTag = `sprint:${fm.id}`;
|
|
15560
|
+
const workItemDocs = allDocs.filter(
|
|
15561
|
+
(d) => d.frontmatter.type !== "sprint" && d.frontmatter.type !== "epic" && d.frontmatter.tags?.includes(sprintTag)
|
|
15562
|
+
);
|
|
15563
|
+
const byStatus = {};
|
|
15564
|
+
const byType = {};
|
|
15565
|
+
let doneCount = 0;
|
|
15566
|
+
let inProgressCount = 0;
|
|
15567
|
+
let openCount = 0;
|
|
15568
|
+
let blockedCount = 0;
|
|
15569
|
+
for (const doc of workItemDocs) {
|
|
15570
|
+
const s = doc.frontmatter.status;
|
|
15571
|
+
byStatus[s] = (byStatus[s] ?? 0) + 1;
|
|
15572
|
+
byType[doc.frontmatter.type] = (byType[doc.frontmatter.type] ?? 0) + 1;
|
|
15573
|
+
if (DONE_STATUSES.has(s)) doneCount++;
|
|
15574
|
+
else if (s === "in-progress") inProgressCount++;
|
|
15575
|
+
else if (s === "blocked") blockedCount++;
|
|
15576
|
+
else openCount++;
|
|
15577
|
+
}
|
|
15578
|
+
const workItems = {
|
|
15579
|
+
total: workItemDocs.length,
|
|
15580
|
+
done: doneCount,
|
|
15581
|
+
inProgress: inProgressCount,
|
|
15582
|
+
open: openCount,
|
|
15583
|
+
blocked: blockedCount,
|
|
15584
|
+
completionPct: workItemDocs.length > 0 ? Math.round(doneCount / workItemDocs.length * 100) : 0,
|
|
15585
|
+
byStatus,
|
|
15586
|
+
byType,
|
|
15587
|
+
items: workItemDocs.map((d) => ({
|
|
15588
|
+
id: d.frontmatter.id,
|
|
15589
|
+
title: d.frontmatter.title,
|
|
15590
|
+
type: d.frontmatter.type,
|
|
15591
|
+
status: d.frontmatter.status
|
|
15592
|
+
}))
|
|
15593
|
+
};
|
|
15594
|
+
const meetings = [];
|
|
15595
|
+
if (startDate && endDate) {
|
|
15596
|
+
const meetingDocs = allDocs.filter((d) => d.frontmatter.type === "meeting");
|
|
15597
|
+
for (const m of meetingDocs) {
|
|
15598
|
+
const meetingDate = m.frontmatter.date ?? m.frontmatter.created.slice(0, 10);
|
|
15599
|
+
if (meetingDate >= startDate && meetingDate <= endDate) {
|
|
15600
|
+
meetings.push({
|
|
15601
|
+
id: m.frontmatter.id,
|
|
15602
|
+
title: m.frontmatter.title,
|
|
15603
|
+
date: meetingDate
|
|
15604
|
+
});
|
|
15605
|
+
}
|
|
15606
|
+
}
|
|
15607
|
+
meetings.sort((a, b) => a.date.localeCompare(b.date));
|
|
15608
|
+
}
|
|
15609
|
+
const artifacts = [];
|
|
15610
|
+
if (startDate && endDate) {
|
|
15611
|
+
for (const doc of allDocs) {
|
|
15612
|
+
if (doc.frontmatter.type === "sprint") continue;
|
|
15613
|
+
const created = doc.frontmatter.created.slice(0, 10);
|
|
15614
|
+
const updated = doc.frontmatter.updated.slice(0, 10);
|
|
15615
|
+
if (created >= startDate && created <= endDate) {
|
|
15616
|
+
artifacts.push({
|
|
15617
|
+
id: doc.frontmatter.id,
|
|
15618
|
+
title: doc.frontmatter.title,
|
|
15619
|
+
type: doc.frontmatter.type,
|
|
15620
|
+
action: "created",
|
|
15621
|
+
date: created
|
|
15622
|
+
});
|
|
15623
|
+
} else if (updated >= startDate && updated <= endDate && updated !== created) {
|
|
15624
|
+
artifacts.push({
|
|
15625
|
+
id: doc.frontmatter.id,
|
|
15626
|
+
title: doc.frontmatter.title,
|
|
15627
|
+
type: doc.frontmatter.type,
|
|
15628
|
+
action: "updated",
|
|
15629
|
+
date: updated
|
|
15630
|
+
});
|
|
15631
|
+
}
|
|
15632
|
+
}
|
|
15633
|
+
artifacts.sort((a, b) => b.date.localeCompare(a.date));
|
|
15634
|
+
}
|
|
15635
|
+
const relevantTags = /* @__PURE__ */ new Set([sprintTag, ...linkedEpicIds.map((id) => `epic:${id}`)]);
|
|
15636
|
+
const openActions = allDocs.filter(
|
|
15637
|
+
(d) => d.frontmatter.type === "action" && !DONE_STATUSES.has(d.frontmatter.status) && d.frontmatter.tags?.some((t) => relevantTags.has(t))
|
|
15638
|
+
).map((d) => ({
|
|
15639
|
+
id: d.frontmatter.id,
|
|
15640
|
+
title: d.frontmatter.title,
|
|
15641
|
+
owner: d.frontmatter.owner,
|
|
15642
|
+
dueDate: d.frontmatter.dueDate
|
|
15643
|
+
}));
|
|
15644
|
+
const openQuestions = allDocs.filter(
|
|
15645
|
+
(d) => d.frontmatter.type === "question" && d.frontmatter.status === "open" && d.frontmatter.tags?.some((t) => relevantTags.has(t))
|
|
15646
|
+
).map((d) => ({
|
|
15647
|
+
id: d.frontmatter.id,
|
|
15648
|
+
title: d.frontmatter.title
|
|
15649
|
+
}));
|
|
15650
|
+
const blockers = allDocs.filter(
|
|
15651
|
+
(d) => d.frontmatter.status === "blocked" && d.frontmatter.tags?.includes(sprintTag)
|
|
15652
|
+
).map((d) => ({
|
|
15653
|
+
id: d.frontmatter.id,
|
|
15654
|
+
title: d.frontmatter.title,
|
|
15655
|
+
type: d.frontmatter.type
|
|
15656
|
+
}));
|
|
15657
|
+
const riskBlockers = allDocs.filter(
|
|
15658
|
+
(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)
|
|
15659
|
+
);
|
|
15660
|
+
for (const d of riskBlockers) {
|
|
15661
|
+
blockers.push({
|
|
15662
|
+
id: d.frontmatter.id,
|
|
15663
|
+
title: d.frontmatter.title,
|
|
15664
|
+
type: d.frontmatter.type
|
|
15665
|
+
});
|
|
15666
|
+
}
|
|
15667
|
+
let velocity = null;
|
|
15668
|
+
const currentRate = workItems.completionPct;
|
|
15669
|
+
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 ?? ""));
|
|
15670
|
+
if (completedSprints.length > 0) {
|
|
15671
|
+
const prev = completedSprints[0];
|
|
15672
|
+
const prevTag = `sprint:${prev.frontmatter.id}`;
|
|
15673
|
+
const prevWorkItems = allDocs.filter(
|
|
15674
|
+
(d) => d.frontmatter.type !== "sprint" && d.frontmatter.type !== "epic" && d.frontmatter.tags?.includes(prevTag)
|
|
15675
|
+
);
|
|
15676
|
+
const prevDone = prevWorkItems.filter((d) => DONE_STATUSES.has(d.frontmatter.status)).length;
|
|
15677
|
+
const prevRate = prevWorkItems.length > 0 ? Math.round(prevDone / prevWorkItems.length * 100) : 0;
|
|
15678
|
+
velocity = {
|
|
15679
|
+
currentCompletionRate: currentRate,
|
|
15680
|
+
previousSprintRate: prevRate,
|
|
15681
|
+
previousSprintId: prev.frontmatter.id
|
|
15682
|
+
};
|
|
15683
|
+
} else {
|
|
15684
|
+
velocity = { currentCompletionRate: currentRate };
|
|
15685
|
+
}
|
|
15686
|
+
return {
|
|
15687
|
+
sprint: {
|
|
15688
|
+
id: fm.id,
|
|
15689
|
+
title: fm.title,
|
|
15690
|
+
goal: fm.goal,
|
|
15691
|
+
status: fm.status,
|
|
15692
|
+
startDate,
|
|
15693
|
+
endDate
|
|
15694
|
+
},
|
|
15695
|
+
timeline: { daysElapsed, daysRemaining, totalDays, percentComplete },
|
|
15696
|
+
linkedEpics,
|
|
15697
|
+
workItems,
|
|
15698
|
+
meetings,
|
|
15699
|
+
artifacts,
|
|
15700
|
+
openActions,
|
|
15701
|
+
openQuestions,
|
|
15702
|
+
blockers,
|
|
15703
|
+
velocity
|
|
15704
|
+
};
|
|
15705
|
+
}
|
|
15706
|
+
|
|
15707
|
+
// src/reports/sprint-summary/generator.ts
|
|
15708
|
+
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
15709
|
+
async function generateSprintSummary(data) {
|
|
15710
|
+
const prompt = buildPrompt(data);
|
|
15711
|
+
const result = query({
|
|
15712
|
+
prompt,
|
|
15713
|
+
options: {
|
|
15714
|
+
systemPrompt: SYSTEM_PROMPT,
|
|
15715
|
+
maxTurns: 1,
|
|
15716
|
+
tools: [],
|
|
15717
|
+
allowedTools: []
|
|
15718
|
+
}
|
|
15719
|
+
});
|
|
15720
|
+
for await (const msg of result) {
|
|
15721
|
+
if (msg.type === "assistant") {
|
|
15722
|
+
const text = msg.message.content.find(
|
|
15723
|
+
(b) => b.type === "text"
|
|
15724
|
+
);
|
|
15725
|
+
if (text) return text.text;
|
|
15726
|
+
}
|
|
15727
|
+
}
|
|
15728
|
+
return "Unable to generate sprint summary.";
|
|
15729
|
+
}
|
|
15730
|
+
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:
|
|
15731
|
+
|
|
15732
|
+
## Sprint Health
|
|
15733
|
+
One-line verdict on overall sprint health (healthy / at risk / behind).
|
|
15734
|
+
|
|
15735
|
+
## Goal Progress
|
|
15736
|
+
How close the team is to achieving the sprint goal. Reference the goal text and completion metrics.
|
|
15737
|
+
|
|
15738
|
+
## Key Achievements
|
|
15739
|
+
Notable completions, decisions made, meetings held during the sprint. Use bullet points.
|
|
15740
|
+
|
|
15741
|
+
## Current Risks
|
|
15742
|
+
Blockers, overdue items, unresolved questions, items without owners. Use bullet points. If none, say so.
|
|
15743
|
+
|
|
15744
|
+
## Outcome Projection
|
|
15745
|
+
Given the current pace and time remaining, what's the likely outcome? Will the sprint goal be met?
|
|
15746
|
+
|
|
15747
|
+
Be specific \u2014 reference artifact IDs, dates, and numbers from the data. Keep the tone professional but direct.`;
|
|
15748
|
+
function buildPrompt(data) {
|
|
15749
|
+
const sections = [];
|
|
15750
|
+
sections.push(`# Sprint: ${data.sprint.id} \u2014 ${data.sprint.title}`);
|
|
15751
|
+
sections.push(`Status: ${data.sprint.status}`);
|
|
15752
|
+
if (data.sprint.goal) sections.push(`Goal: ${data.sprint.goal}`);
|
|
15753
|
+
if (data.sprint.startDate) sections.push(`Start: ${data.sprint.startDate}`);
|
|
15754
|
+
if (data.sprint.endDate) sections.push(`End: ${data.sprint.endDate}`);
|
|
15755
|
+
sections.push(`
|
|
15756
|
+
## Timeline`);
|
|
15757
|
+
sections.push(`Days elapsed: ${data.timeline.daysElapsed} / ${data.timeline.totalDays}`);
|
|
15758
|
+
sections.push(`Days remaining: ${data.timeline.daysRemaining}`);
|
|
15759
|
+
sections.push(`Timeline progress: ${data.timeline.percentComplete}%`);
|
|
15760
|
+
sections.push(`
|
|
15761
|
+
## Work Items`);
|
|
15762
|
+
sections.push(`Total: ${data.workItems.total}, Done: ${data.workItems.done}, In Progress: ${data.workItems.inProgress}, Open: ${data.workItems.open}, Blocked: ${data.workItems.blocked}`);
|
|
15763
|
+
sections.push(`Completion: ${data.workItems.completionPct}%`);
|
|
15764
|
+
if (Object.keys(data.workItems.byType).length > 0) {
|
|
15765
|
+
sections.push(`By type: ${Object.entries(data.workItems.byType).map(([t, n]) => `${t}: ${n}`).join(", ")}`);
|
|
15766
|
+
}
|
|
15767
|
+
if (data.linkedEpics.length > 0) {
|
|
15768
|
+
sections.push(`
|
|
15769
|
+
## Linked Epics`);
|
|
15770
|
+
for (const e of data.linkedEpics) {
|
|
15771
|
+
sections.push(`- ${e.id}: ${e.title} [${e.status}] \u2014 ${e.tasksDone}/${e.tasksTotal} tasks done`);
|
|
15772
|
+
}
|
|
15773
|
+
}
|
|
15774
|
+
if (data.meetings.length > 0) {
|
|
15775
|
+
sections.push(`
|
|
15776
|
+
## Meetings During Sprint`);
|
|
15777
|
+
for (const m of data.meetings) {
|
|
15778
|
+
sections.push(`- ${m.date}: ${m.id} \u2014 ${m.title}`);
|
|
15779
|
+
}
|
|
15780
|
+
}
|
|
15781
|
+
if (data.artifacts.length > 0) {
|
|
15782
|
+
sections.push(`
|
|
15783
|
+
## Artifacts Created/Updated During Sprint`);
|
|
15784
|
+
for (const a of data.artifacts.slice(0, 20)) {
|
|
15785
|
+
sections.push(`- ${a.date}: ${a.id} (${a.type}) ${a.action} \u2014 ${a.title}`);
|
|
15786
|
+
}
|
|
15787
|
+
if (data.artifacts.length > 20) {
|
|
15788
|
+
sections.push(`... and ${data.artifacts.length - 20} more`);
|
|
15789
|
+
}
|
|
15790
|
+
}
|
|
15791
|
+
if (data.openActions.length > 0) {
|
|
15792
|
+
sections.push(`
|
|
15793
|
+
## Open Actions`);
|
|
15794
|
+
for (const a of data.openActions) {
|
|
15795
|
+
const owner = a.owner ?? "unowned";
|
|
15796
|
+
const due = a.dueDate ?? "no due date";
|
|
15797
|
+
sections.push(`- ${a.id}: ${a.title} (${owner}, ${due})`);
|
|
15798
|
+
}
|
|
15799
|
+
}
|
|
15800
|
+
if (data.openQuestions.length > 0) {
|
|
15801
|
+
sections.push(`
|
|
15802
|
+
## Open Questions`);
|
|
15803
|
+
for (const q of data.openQuestions) {
|
|
15804
|
+
sections.push(`- ${q.id}: ${q.title}`);
|
|
15805
|
+
}
|
|
15806
|
+
}
|
|
15807
|
+
if (data.blockers.length > 0) {
|
|
15808
|
+
sections.push(`
|
|
15809
|
+
## Blockers`);
|
|
15810
|
+
for (const b of data.blockers) {
|
|
15811
|
+
sections.push(`- ${b.id} (${b.type}): ${b.title}`);
|
|
15812
|
+
}
|
|
15813
|
+
}
|
|
15814
|
+
if (data.velocity) {
|
|
15815
|
+
sections.push(`
|
|
15816
|
+
## Velocity`);
|
|
15817
|
+
sections.push(`Current sprint completion rate: ${data.velocity.currentCompletionRate}%`);
|
|
15818
|
+
if (data.velocity.previousSprintRate !== void 0) {
|
|
15819
|
+
sections.push(`Previous sprint (${data.velocity.previousSprintId}): ${data.velocity.previousSprintRate}%`);
|
|
15820
|
+
}
|
|
15821
|
+
}
|
|
15822
|
+
return sections.join("\n");
|
|
15823
|
+
}
|
|
15824
|
+
|
|
15485
15825
|
// src/plugins/builtin/tools/reports.ts
|
|
15486
15826
|
function createReportTools(store) {
|
|
15487
15827
|
return [
|
|
@@ -15773,6 +16113,25 @@ function createReportTools(store) {
|
|
|
15773
16113
|
},
|
|
15774
16114
|
{ annotations: { readOnlyHint: true } }
|
|
15775
16115
|
),
|
|
16116
|
+
tool8(
|
|
16117
|
+
"generate_sprint_summary",
|
|
16118
|
+
"Generate an AI-powered narrative summary of a sprint's progress, health, achievements, risks, and projected outcome",
|
|
16119
|
+
{
|
|
16120
|
+
sprint: external_exports.string().optional().describe("Sprint ID (e.g. 'SP-001'). Omit for the active sprint.")
|
|
16121
|
+
},
|
|
16122
|
+
async (args) => {
|
|
16123
|
+
const data = collectSprintSummaryData(store, args.sprint);
|
|
16124
|
+
if (!data) {
|
|
16125
|
+
const msg = args.sprint ? `Sprint ${args.sprint} not found.` : "No active sprint found.";
|
|
16126
|
+
return { content: [{ type: "text", text: msg }], isError: true };
|
|
16127
|
+
}
|
|
16128
|
+
const summary = await generateSprintSummary(data);
|
|
16129
|
+
return {
|
|
16130
|
+
content: [{ type: "text", text: summary }]
|
|
16131
|
+
};
|
|
16132
|
+
},
|
|
16133
|
+
{ annotations: { readOnlyHint: true } }
|
|
16134
|
+
),
|
|
15776
16135
|
tool8(
|
|
15777
16136
|
"save_report",
|
|
15778
16137
|
"Save a generated report as a persistent document",
|
|
@@ -16616,26 +16975,6 @@ function createSprintPlanningTools(store) {
|
|
|
16616
16975
|
|
|
16617
16976
|
// src/plugins/builtin/tools/tasks.ts
|
|
16618
16977
|
import { tool as tool14 } from "@anthropic-ai/claude-agent-sdk";
|
|
16619
|
-
|
|
16620
|
-
// src/plugins/builtin/tools/task-utils.ts
|
|
16621
|
-
function normalizeLinkedEpics(value) {
|
|
16622
|
-
if (value === void 0 || value === null) return [];
|
|
16623
|
-
if (typeof value === "string") {
|
|
16624
|
-
try {
|
|
16625
|
-
const parsed = JSON.parse(value);
|
|
16626
|
-
if (Array.isArray(parsed)) return parsed.filter((v) => typeof v === "string");
|
|
16627
|
-
} catch {
|
|
16628
|
-
}
|
|
16629
|
-
return [value];
|
|
16630
|
-
}
|
|
16631
|
-
if (Array.isArray(value)) return value.filter((v) => typeof v === "string");
|
|
16632
|
-
return [];
|
|
16633
|
-
}
|
|
16634
|
-
function generateEpicTags(epics) {
|
|
16635
|
-
return epics.map((id) => `epic:${id}`);
|
|
16636
|
-
}
|
|
16637
|
-
|
|
16638
|
-
// src/plugins/builtin/tools/tasks.ts
|
|
16639
16978
|
var linkedEpicArray = external_exports.preprocess(
|
|
16640
16979
|
(val) => {
|
|
16641
16980
|
if (typeof val === "string") {
|
|
@@ -18502,8 +18841,8 @@ function gatherContext(store, focusFeature, includeDecisions = true, includeQues
|
|
|
18502
18841
|
title: e.frontmatter.title,
|
|
18503
18842
|
status: e.frontmatter.status,
|
|
18504
18843
|
linkedFeature: normalizeLinkedFeatures(e.frontmatter.linkedFeature),
|
|
18505
|
-
targetDate: e.frontmatter.targetDate
|
|
18506
|
-
estimatedEffort: e.frontmatter.estimatedEffort
|
|
18844
|
+
targetDate: typeof e.frontmatter.targetDate === "string" ? e.frontmatter.targetDate : null,
|
|
18845
|
+
estimatedEffort: typeof e.frontmatter.estimatedEffort === "string" ? e.frontmatter.estimatedEffort : null,
|
|
18507
18846
|
content: e.content,
|
|
18508
18847
|
linkedTaskCount: tasks.filter(
|
|
18509
18848
|
(t) => normalizeLinkedEpics(t.frontmatter.linkedEpic).includes(e.frontmatter.id)
|
|
@@ -18514,10 +18853,10 @@ function gatherContext(store, focusFeature, includeDecisions = true, includeQues
|
|
|
18514
18853
|
title: t.frontmatter.title,
|
|
18515
18854
|
status: t.frontmatter.status,
|
|
18516
18855
|
linkedEpic: normalizeLinkedEpics(t.frontmatter.linkedEpic),
|
|
18517
|
-
acceptanceCriteria: t.frontmatter.acceptanceCriteria
|
|
18518
|
-
technicalNotes: t.frontmatter.technicalNotes
|
|
18519
|
-
complexity: t.frontmatter.complexity
|
|
18520
|
-
estimatedPoints: t.frontmatter.estimatedPoints
|
|
18856
|
+
acceptanceCriteria: typeof t.frontmatter.acceptanceCriteria === "string" ? t.frontmatter.acceptanceCriteria : null,
|
|
18857
|
+
technicalNotes: typeof t.frontmatter.technicalNotes === "string" ? t.frontmatter.technicalNotes : null,
|
|
18858
|
+
complexity: typeof t.frontmatter.complexity === "string" ? t.frontmatter.complexity : null,
|
|
18859
|
+
estimatedPoints: typeof t.frontmatter.estimatedPoints === "number" ? t.frontmatter.estimatedPoints : null,
|
|
18521
18860
|
priority: t.frontmatter.priority ?? null
|
|
18522
18861
|
})),
|
|
18523
18862
|
decisions: allDecisions.map((d) => ({
|
|
@@ -19020,7 +19359,7 @@ ${fragment}`);
|
|
|
19020
19359
|
import { tool as tool23 } from "@anthropic-ai/claude-agent-sdk";
|
|
19021
19360
|
|
|
19022
19361
|
// src/skills/action-runner.ts
|
|
19023
|
-
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
19362
|
+
import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
|
|
19024
19363
|
|
|
19025
19364
|
// src/agent/mcp-server.ts
|
|
19026
19365
|
import {
|
|
@@ -19152,7 +19491,7 @@ function computeUrgency(dueDateStr, todayStr) {
|
|
|
19152
19491
|
if (diffDays <= 14) return "upcoming";
|
|
19153
19492
|
return "later";
|
|
19154
19493
|
}
|
|
19155
|
-
var
|
|
19494
|
+
var DONE_STATUSES2 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
19156
19495
|
function getUpcomingData(store) {
|
|
19157
19496
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
19158
19497
|
const allDocs = store.list();
|
|
@@ -19161,7 +19500,7 @@ function getUpcomingData(store) {
|
|
|
19161
19500
|
docById.set(doc.frontmatter.id, doc);
|
|
19162
19501
|
}
|
|
19163
19502
|
const actions = allDocs.filter(
|
|
19164
|
-
(d) => d.frontmatter.type === "action" && !
|
|
19503
|
+
(d) => d.frontmatter.type === "action" && !DONE_STATUSES2.has(d.frontmatter.status)
|
|
19165
19504
|
);
|
|
19166
19505
|
const actionsWithDue = actions.filter((d) => d.frontmatter.dueDate);
|
|
19167
19506
|
const sprints = allDocs.filter((d) => d.frontmatter.type === "sprint");
|
|
@@ -19225,7 +19564,7 @@ function getUpcomingData(store) {
|
|
|
19225
19564
|
const sprintEnd = sprint.frontmatter.endDate;
|
|
19226
19565
|
const sprintTaskDocs = getSprintTasks(sprint);
|
|
19227
19566
|
for (const task of sprintTaskDocs) {
|
|
19228
|
-
if (
|
|
19567
|
+
if (DONE_STATUSES2.has(task.frontmatter.status)) continue;
|
|
19229
19568
|
const existing = taskSprintMap.get(task.frontmatter.id);
|
|
19230
19569
|
if (!existing || sprintEnd < existing.sprintEnd) {
|
|
19231
19570
|
taskSprintMap.set(task.frontmatter.id, { task, sprint, sprintEnd });
|
|
@@ -19242,7 +19581,7 @@ function getUpcomingData(store) {
|
|
|
19242
19581
|
urgency: computeUrgency(sprintEnd, today)
|
|
19243
19582
|
})).sort((a, b) => a.sprintEndDate.localeCompare(b.sprintEndDate));
|
|
19244
19583
|
const openItems = allDocs.filter(
|
|
19245
|
-
(d) => ["action", "question", "task"].includes(d.frontmatter.type) && !
|
|
19584
|
+
(d) => ["action", "question", "task"].includes(d.frontmatter.type) && !DONE_STATUSES2.has(d.frontmatter.status)
|
|
19246
19585
|
);
|
|
19247
19586
|
const fourteenDaysAgo = new Date(todayMs - fourteenDaysMs).toISOString().slice(0, 10);
|
|
19248
19587
|
const recentMeetings = allDocs.filter(
|
|
@@ -19340,8 +19679,28 @@ function getUpcomingData(store) {
|
|
|
19340
19679
|
}).filter((item) => item.score > 0).sort((a, b) => b.score - a.score).slice(0, 15);
|
|
19341
19680
|
return { dueSoonActions, dueSoonSprintTasks, trending };
|
|
19342
19681
|
}
|
|
19682
|
+
function getSprintSummaryData(store, sprintId) {
|
|
19683
|
+
return collectSprintSummaryData(store, sprintId);
|
|
19684
|
+
}
|
|
19343
19685
|
|
|
19344
19686
|
// src/web/templates/layout.ts
|
|
19687
|
+
function collapsibleSection(sectionId, title, content, opts) {
|
|
19688
|
+
const tag = opts?.titleTag ?? "div";
|
|
19689
|
+
const cls = opts?.titleClass ?? "section-title";
|
|
19690
|
+
const collapsed = opts?.defaultCollapsed ? " collapsed" : "";
|
|
19691
|
+
return `
|
|
19692
|
+
<div class="collapsible${collapsed}" data-section-id="${escapeHtml(sectionId)}">
|
|
19693
|
+
<${tag} class="${cls} collapsible-header" onclick="toggleSection(this)">
|
|
19694
|
+
<svg class="collapsible-chevron" viewBox="0 0 16 16" width="14" height="14" fill="currentColor">
|
|
19695
|
+
<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"/>
|
|
19696
|
+
</svg>
|
|
19697
|
+
<span>${title}</span>
|
|
19698
|
+
</${tag}>
|
|
19699
|
+
<div class="collapsible-body">
|
|
19700
|
+
${content}
|
|
19701
|
+
</div>
|
|
19702
|
+
</div>`;
|
|
19703
|
+
}
|
|
19345
19704
|
function escapeHtml(str) {
|
|
19346
19705
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
19347
19706
|
}
|
|
@@ -19464,6 +19823,7 @@ function layout(opts, body) {
|
|
|
19464
19823
|
const topItems = [
|
|
19465
19824
|
{ href: "/", label: "Overview" },
|
|
19466
19825
|
{ href: "/upcoming", label: "Upcoming" },
|
|
19826
|
+
{ href: "/sprint-summary", label: "Sprint Summary" },
|
|
19467
19827
|
{ href: "/timeline", label: "Timeline" },
|
|
19468
19828
|
{ href: "/board", label: "Board" },
|
|
19469
19829
|
{ href: "/gar", label: "GAR Report" },
|
|
@@ -19509,6 +19869,32 @@ function layout(opts, body) {
|
|
|
19509
19869
|
${body}
|
|
19510
19870
|
</main>
|
|
19511
19871
|
</div>
|
|
19872
|
+
<script>
|
|
19873
|
+
function toggleSection(header) {
|
|
19874
|
+
var section = header.closest('.collapsible');
|
|
19875
|
+
if (!section) return;
|
|
19876
|
+
section.classList.toggle('collapsed');
|
|
19877
|
+
var id = section.getAttribute('data-section-id');
|
|
19878
|
+
if (id) {
|
|
19879
|
+
try {
|
|
19880
|
+
var state = JSON.parse(localStorage.getItem('marvin-collapsed') || '{}');
|
|
19881
|
+
state[id] = section.classList.contains('collapsed');
|
|
19882
|
+
localStorage.setItem('marvin-collapsed', JSON.stringify(state));
|
|
19883
|
+
} catch(e) {}
|
|
19884
|
+
}
|
|
19885
|
+
}
|
|
19886
|
+
// Restore collapsed state on load
|
|
19887
|
+
(function() {
|
|
19888
|
+
try {
|
|
19889
|
+
var state = JSON.parse(localStorage.getItem('marvin-collapsed') || '{}');
|
|
19890
|
+
document.querySelectorAll('.collapsible[data-section-id]').forEach(function(el) {
|
|
19891
|
+
var id = el.getAttribute('data-section-id');
|
|
19892
|
+
if (state[id] === true) el.classList.add('collapsed');
|
|
19893
|
+
else if (state[id] === false) el.classList.remove('collapsed');
|
|
19894
|
+
});
|
|
19895
|
+
} catch(e) {}
|
|
19896
|
+
})();
|
|
19897
|
+
</script>
|
|
19512
19898
|
<script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
|
|
19513
19899
|
<script>mermaid.initialize({
|
|
19514
19900
|
startOnLoad: true,
|
|
@@ -20332,13 +20718,60 @@ tr:hover td {
|
|
|
20332
20718
|
white-space: nowrap;
|
|
20333
20719
|
}
|
|
20334
20720
|
|
|
20721
|
+
.gantt-grid-line {
|
|
20722
|
+
position: absolute;
|
|
20723
|
+
top: 0;
|
|
20724
|
+
bottom: 0;
|
|
20725
|
+
width: 1px;
|
|
20726
|
+
background: var(--border);
|
|
20727
|
+
opacity: 0.35;
|
|
20728
|
+
}
|
|
20729
|
+
|
|
20730
|
+
.gantt-sprint-line {
|
|
20731
|
+
position: absolute;
|
|
20732
|
+
top: 0;
|
|
20733
|
+
bottom: 0;
|
|
20734
|
+
width: 1px;
|
|
20735
|
+
background: var(--text-dim);
|
|
20736
|
+
opacity: 0.3;
|
|
20737
|
+
}
|
|
20738
|
+
|
|
20335
20739
|
.gantt-today {
|
|
20336
20740
|
position: absolute;
|
|
20337
20741
|
top: 0;
|
|
20338
20742
|
bottom: 0;
|
|
20339
|
-
width:
|
|
20743
|
+
width: 3px;
|
|
20340
20744
|
background: var(--red);
|
|
20341
|
-
opacity: 0.
|
|
20745
|
+
opacity: 0.8;
|
|
20746
|
+
border-radius: 1px;
|
|
20747
|
+
}
|
|
20748
|
+
|
|
20749
|
+
/* Sprint band in timeline */
|
|
20750
|
+
.gantt-sprint-band-row {
|
|
20751
|
+
border-bottom: 1px solid var(--border);
|
|
20752
|
+
margin-bottom: 0.25rem;
|
|
20753
|
+
}
|
|
20754
|
+
|
|
20755
|
+
.gantt-sprint-band {
|
|
20756
|
+
height: 32px;
|
|
20757
|
+
}
|
|
20758
|
+
|
|
20759
|
+
.gantt-sprint-block {
|
|
20760
|
+
position: absolute;
|
|
20761
|
+
top: 2px;
|
|
20762
|
+
bottom: 2px;
|
|
20763
|
+
background: var(--bg-hover);
|
|
20764
|
+
border: 1px solid var(--border);
|
|
20765
|
+
border-radius: 4px;
|
|
20766
|
+
font-size: 0.65rem;
|
|
20767
|
+
color: var(--text-dim);
|
|
20768
|
+
display: flex;
|
|
20769
|
+
align-items: center;
|
|
20770
|
+
justify-content: center;
|
|
20771
|
+
overflow: hidden;
|
|
20772
|
+
white-space: nowrap;
|
|
20773
|
+
text-overflow: ellipsis;
|
|
20774
|
+
padding: 0 0.4rem;
|
|
20342
20775
|
}
|
|
20343
20776
|
|
|
20344
20777
|
/* Pie chart color overrides */
|
|
@@ -20400,6 +20833,146 @@ tr:hover td {
|
|
|
20400
20833
|
}
|
|
20401
20834
|
|
|
20402
20835
|
.text-dim { color: var(--text-dim); }
|
|
20836
|
+
|
|
20837
|
+
/* Sprint Summary */
|
|
20838
|
+
.sprint-goal {
|
|
20839
|
+
background: var(--bg-card);
|
|
20840
|
+
border: 1px solid var(--border);
|
|
20841
|
+
border-radius: var(--radius);
|
|
20842
|
+
padding: 0.75rem 1rem;
|
|
20843
|
+
margin-bottom: 1rem;
|
|
20844
|
+
font-size: 0.9rem;
|
|
20845
|
+
color: var(--text);
|
|
20846
|
+
}
|
|
20847
|
+
|
|
20848
|
+
.sprint-progress-bar {
|
|
20849
|
+
position: relative;
|
|
20850
|
+
height: 24px;
|
|
20851
|
+
background: var(--bg-card);
|
|
20852
|
+
border: 1px solid var(--border);
|
|
20853
|
+
border-radius: 12px;
|
|
20854
|
+
margin-bottom: 1.25rem;
|
|
20855
|
+
overflow: hidden;
|
|
20856
|
+
}
|
|
20857
|
+
|
|
20858
|
+
.sprint-progress-fill {
|
|
20859
|
+
height: 100%;
|
|
20860
|
+
background: linear-gradient(90deg, var(--accent-dim), var(--accent));
|
|
20861
|
+
border-radius: 12px;
|
|
20862
|
+
transition: width 0.3s ease;
|
|
20863
|
+
}
|
|
20864
|
+
|
|
20865
|
+
.sprint-progress-label {
|
|
20866
|
+
position: absolute;
|
|
20867
|
+
top: 50%;
|
|
20868
|
+
left: 50%;
|
|
20869
|
+
transform: translate(-50%, -50%);
|
|
20870
|
+
font-size: 0.7rem;
|
|
20871
|
+
font-weight: 700;
|
|
20872
|
+
color: var(--text);
|
|
20873
|
+
}
|
|
20874
|
+
|
|
20875
|
+
.sprint-ai-section {
|
|
20876
|
+
margin-top: 2rem;
|
|
20877
|
+
background: var(--bg-card);
|
|
20878
|
+
border: 1px solid var(--border);
|
|
20879
|
+
border-radius: var(--radius);
|
|
20880
|
+
padding: 1.5rem;
|
|
20881
|
+
}
|
|
20882
|
+
|
|
20883
|
+
.sprint-ai-section h3 {
|
|
20884
|
+
font-size: 1rem;
|
|
20885
|
+
font-weight: 600;
|
|
20886
|
+
margin-bottom: 0.5rem;
|
|
20887
|
+
}
|
|
20888
|
+
|
|
20889
|
+
.sprint-generate-btn {
|
|
20890
|
+
background: var(--accent);
|
|
20891
|
+
color: #fff;
|
|
20892
|
+
border: none;
|
|
20893
|
+
border-radius: var(--radius);
|
|
20894
|
+
padding: 0.5rem 1.25rem;
|
|
20895
|
+
font-size: 0.85rem;
|
|
20896
|
+
font-weight: 600;
|
|
20897
|
+
cursor: pointer;
|
|
20898
|
+
margin-top: 0.75rem;
|
|
20899
|
+
transition: background 0.15s;
|
|
20900
|
+
}
|
|
20901
|
+
|
|
20902
|
+
.sprint-generate-btn:hover:not(:disabled) {
|
|
20903
|
+
background: var(--accent-dim);
|
|
20904
|
+
}
|
|
20905
|
+
|
|
20906
|
+
.sprint-generate-btn:disabled {
|
|
20907
|
+
opacity: 0.5;
|
|
20908
|
+
cursor: not-allowed;
|
|
20909
|
+
}
|
|
20910
|
+
|
|
20911
|
+
.sprint-loading {
|
|
20912
|
+
display: flex;
|
|
20913
|
+
align-items: center;
|
|
20914
|
+
gap: 0.75rem;
|
|
20915
|
+
padding: 1rem 0;
|
|
20916
|
+
color: var(--text-dim);
|
|
20917
|
+
font-size: 0.85rem;
|
|
20918
|
+
}
|
|
20919
|
+
|
|
20920
|
+
.sprint-spinner {
|
|
20921
|
+
width: 20px;
|
|
20922
|
+
height: 20px;
|
|
20923
|
+
border: 2px solid var(--border);
|
|
20924
|
+
border-top-color: var(--accent);
|
|
20925
|
+
border-radius: 50%;
|
|
20926
|
+
animation: sprint-spin 0.8s linear infinite;
|
|
20927
|
+
}
|
|
20928
|
+
|
|
20929
|
+
@keyframes sprint-spin {
|
|
20930
|
+
to { transform: rotate(360deg); }
|
|
20931
|
+
}
|
|
20932
|
+
|
|
20933
|
+
.sprint-error {
|
|
20934
|
+
color: var(--red);
|
|
20935
|
+
font-size: 0.85rem;
|
|
20936
|
+
padding: 0.5rem 0;
|
|
20937
|
+
}
|
|
20938
|
+
|
|
20939
|
+
.sprint-ai-section .detail-content {
|
|
20940
|
+
margin-top: 1rem;
|
|
20941
|
+
}
|
|
20942
|
+
|
|
20943
|
+
/* Collapsible sections */
|
|
20944
|
+
.collapsible-header {
|
|
20945
|
+
cursor: pointer;
|
|
20946
|
+
display: flex;
|
|
20947
|
+
align-items: center;
|
|
20948
|
+
gap: 0.4rem;
|
|
20949
|
+
user-select: none;
|
|
20950
|
+
}
|
|
20951
|
+
|
|
20952
|
+
.collapsible-header:hover {
|
|
20953
|
+
color: var(--accent);
|
|
20954
|
+
}
|
|
20955
|
+
|
|
20956
|
+
.collapsible-chevron {
|
|
20957
|
+
transition: transform 0.2s ease;
|
|
20958
|
+
flex-shrink: 0;
|
|
20959
|
+
}
|
|
20960
|
+
|
|
20961
|
+
.collapsible.collapsed .collapsible-chevron {
|
|
20962
|
+
transform: rotate(-90deg);
|
|
20963
|
+
}
|
|
20964
|
+
|
|
20965
|
+
.collapsible-body {
|
|
20966
|
+
overflow: hidden;
|
|
20967
|
+
max-height: 5000px;
|
|
20968
|
+
transition: max-height 0.3s ease, opacity 0.2s ease;
|
|
20969
|
+
opacity: 1;
|
|
20970
|
+
}
|
|
20971
|
+
|
|
20972
|
+
.collapsible.collapsed .collapsible-body {
|
|
20973
|
+
max-height: 0;
|
|
20974
|
+
opacity: 0;
|
|
20975
|
+
}
|
|
20403
20976
|
`;
|
|
20404
20977
|
}
|
|
20405
20978
|
|
|
@@ -20452,35 +21025,73 @@ function buildTimelineGantt(data, maxSprints = 6) {
|
|
|
20452
21025
|
);
|
|
20453
21026
|
tick += 7 * DAY;
|
|
20454
21027
|
}
|
|
21028
|
+
const gridLines = [];
|
|
21029
|
+
let gridTick = timelineStart;
|
|
21030
|
+
const gridStartDay = new Date(gridTick).getDay();
|
|
21031
|
+
gridTick += (8 - gridStartDay) % 7 * DAY;
|
|
21032
|
+
while (gridTick <= timelineEnd) {
|
|
21033
|
+
gridLines.push(`<div class="gantt-grid-line" style="left:${pct(gridTick).toFixed(2)}%"></div>`);
|
|
21034
|
+
gridTick += 7 * DAY;
|
|
21035
|
+
}
|
|
21036
|
+
const sprintBoundaries = /* @__PURE__ */ new Set();
|
|
21037
|
+
for (const sprint of visibleSprints) {
|
|
21038
|
+
sprintBoundaries.add(toMs(sprint.startDate));
|
|
21039
|
+
sprintBoundaries.add(toMs(sprint.endDate));
|
|
21040
|
+
}
|
|
21041
|
+
const sprintLines = [...sprintBoundaries].map(
|
|
21042
|
+
(ms) => `<div class="gantt-sprint-line" style="left:${pct(ms).toFixed(2)}%"></div>`
|
|
21043
|
+
);
|
|
20455
21044
|
const now = Date.now();
|
|
20456
21045
|
let todayMarker = "";
|
|
20457
21046
|
if (now >= timelineStart && now <= timelineEnd) {
|
|
20458
21047
|
todayMarker = `<div class="gantt-today" style="left:${pct(now).toFixed(2)}%"></div>`;
|
|
20459
21048
|
}
|
|
20460
|
-
const
|
|
21049
|
+
const sprintBlocks = visibleSprints.map((sprint) => {
|
|
21050
|
+
const sStart = toMs(sprint.startDate);
|
|
21051
|
+
const sEnd = toMs(sprint.endDate);
|
|
21052
|
+
const left = pct(sStart).toFixed(2);
|
|
21053
|
+
const width = (pct(sEnd) - pct(sStart)).toFixed(2);
|
|
21054
|
+
return `<div class="gantt-sprint-block" style="left:${left}%;width:${width}%">${sanitize(sprint.id, 20)}</div>`;
|
|
21055
|
+
}).join("");
|
|
21056
|
+
const sprintBandRow = `<div class="gantt-row gantt-sprint-band-row">
|
|
21057
|
+
<div class="gantt-label gantt-section-label">Sprints</div>
|
|
21058
|
+
<div class="gantt-track gantt-sprint-band">${sprintBlocks}</div>
|
|
21059
|
+
</div>`;
|
|
21060
|
+
const epicSpanMap = /* @__PURE__ */ new Map();
|
|
20461
21061
|
for (const sprint of visibleSprints) {
|
|
20462
21062
|
const sStart = toMs(sprint.startDate);
|
|
20463
21063
|
const sEnd = toMs(sprint.endDate);
|
|
20464
|
-
|
|
20465
|
-
|
|
20466
|
-
|
|
20467
|
-
|
|
20468
|
-
|
|
20469
|
-
|
|
20470
|
-
|
|
20471
|
-
|
|
20472
|
-
|
|
20473
|
-
|
|
20474
|
-
|
|
20475
|
-
|
|
20476
|
-
|
|
20477
|
-
|
|
21064
|
+
for (const eid of sprint.linkedEpics) {
|
|
21065
|
+
if (!epicMap.has(eid)) continue;
|
|
21066
|
+
const existing = epicSpanMap.get(eid);
|
|
21067
|
+
if (existing) {
|
|
21068
|
+
existing.startMs = Math.min(existing.startMs, sStart);
|
|
21069
|
+
existing.endMs = Math.max(existing.endMs, sEnd);
|
|
21070
|
+
} else {
|
|
21071
|
+
epicSpanMap.set(eid, { startMs: sStart, endMs: sEnd });
|
|
21072
|
+
}
|
|
21073
|
+
}
|
|
21074
|
+
}
|
|
21075
|
+
const sortedEpicIds = [...epicSpanMap.keys()].sort((a, b) => {
|
|
21076
|
+
const aSpan = epicSpanMap.get(a);
|
|
21077
|
+
const bSpan = epicSpanMap.get(b);
|
|
21078
|
+
if (aSpan.startMs !== bSpan.startMs) return aSpan.startMs - bSpan.startMs;
|
|
21079
|
+
return a.localeCompare(b);
|
|
21080
|
+
});
|
|
21081
|
+
const epicRows = sortedEpicIds.map((eid) => {
|
|
21082
|
+
const epic = epicMap.get(eid);
|
|
21083
|
+
const { startMs, endMs } = epicSpanMap.get(eid);
|
|
21084
|
+
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";
|
|
21085
|
+
const left = pct(startMs).toFixed(2);
|
|
21086
|
+
const width = (pct(endMs) - pct(startMs)).toFixed(2);
|
|
21087
|
+
const label = sanitize(epic.id + " " + epic.title);
|
|
21088
|
+
return `<div class="gantt-row">
|
|
21089
|
+
<div class="gantt-label">${label}</div>
|
|
20478
21090
|
<div class="gantt-track">
|
|
20479
21091
|
<div class="gantt-bar ${cls}" style="left:${left}%;width:${width}%"></div>
|
|
20480
21092
|
</div>
|
|
20481
|
-
</div
|
|
20482
|
-
|
|
20483
|
-
}
|
|
21093
|
+
</div>`;
|
|
21094
|
+
}).join("\n");
|
|
20484
21095
|
const note = truncated ? `<div class="mermaid-note">${hiddenCount} earlier sprint${hiddenCount > 1 ? "s" : ""} not shown</div>` : "";
|
|
20485
21096
|
return `${note}
|
|
20486
21097
|
<div class="gantt">
|
|
@@ -20489,11 +21100,12 @@ function buildTimelineGantt(data, maxSprints = 6) {
|
|
|
20489
21100
|
<div class="gantt-label"></div>
|
|
20490
21101
|
<div class="gantt-track gantt-dates">${markers.join("")}</div>
|
|
20491
21102
|
</div>
|
|
20492
|
-
${
|
|
21103
|
+
${sprintBandRow}
|
|
21104
|
+
${epicRows}
|
|
20493
21105
|
</div>
|
|
20494
21106
|
<div class="gantt-overlay">
|
|
20495
21107
|
<div class="gantt-label"></div>
|
|
20496
|
-
<div class="gantt-track">${todayMarker}</div>
|
|
21108
|
+
<div class="gantt-track">${gridLines.join("")}${sprintLines.join("")}${todayMarker}</div>
|
|
20497
21109
|
</div>
|
|
20498
21110
|
</div>`;
|
|
20499
21111
|
}
|
|
@@ -20773,11 +21385,12 @@ function overviewPage(data, diagrams, navGroups) {
|
|
|
20773
21385
|
|
|
20774
21386
|
<div class="section-title"><a href="/timeline">Project Timeline →</a></div>
|
|
20775
21387
|
|
|
20776
|
-
|
|
20777
|
-
${buildArtifactFlowchart(diagrams)}
|
|
21388
|
+
${collapsibleSection("overview-relationships", "Artifact Relationships", buildArtifactFlowchart(diagrams))}
|
|
20778
21389
|
|
|
20779
|
-
|
|
20780
|
-
|
|
21390
|
+
${collapsibleSection(
|
|
21391
|
+
"overview-recent",
|
|
21392
|
+
"Recent Activity",
|
|
21393
|
+
data.recent.length > 0 ? `
|
|
20781
21394
|
<div class="table-wrap">
|
|
20782
21395
|
<table>
|
|
20783
21396
|
<thead>
|
|
@@ -20793,7 +21406,8 @@ function overviewPage(data, diagrams, navGroups) {
|
|
|
20793
21406
|
${rows}
|
|
20794
21407
|
</tbody>
|
|
20795
21408
|
</table>
|
|
20796
|
-
</div>` : `<div class="empty"><p>No documents yet.</p></div>`
|
|
21409
|
+
</div>` : `<div class="empty"><p>No documents yet.</p></div>`
|
|
21410
|
+
)}
|
|
20797
21411
|
`;
|
|
20798
21412
|
}
|
|
20799
21413
|
|
|
@@ -20938,23 +21552,24 @@ function garPage(report) {
|
|
|
20938
21552
|
<div class="label">Overall: ${escapeHtml(report.overall)}</div>
|
|
20939
21553
|
</div>
|
|
20940
21554
|
|
|
20941
|
-
|
|
20942
|
-
${areaCards}
|
|
20943
|
-
</div>
|
|
21555
|
+
${collapsibleSection("gar-areas", "Areas", `<div class="gar-areas">${areaCards}</div>`)}
|
|
20944
21556
|
|
|
20945
|
-
|
|
20946
|
-
|
|
20947
|
-
|
|
20948
|
-
|
|
20949
|
-
|
|
20950
|
-
|
|
21557
|
+
${collapsibleSection(
|
|
21558
|
+
"gar-status-dist",
|
|
21559
|
+
"Status Distribution",
|
|
21560
|
+
buildStatusPie("Action Status", {
|
|
21561
|
+
Open: report.metrics.scope.open,
|
|
21562
|
+
Done: report.metrics.scope.done,
|
|
21563
|
+
"In Progress": Math.max(0, report.metrics.scope.total - report.metrics.scope.open - report.metrics.scope.done)
|
|
21564
|
+
})
|
|
21565
|
+
)}
|
|
20951
21566
|
`;
|
|
20952
21567
|
}
|
|
20953
21568
|
|
|
20954
21569
|
// src/web/templates/pages/health.ts
|
|
20955
21570
|
function healthPage(report, metrics) {
|
|
20956
21571
|
const dotClass = `dot-${report.overall}`;
|
|
20957
|
-
function renderSection(title, categories) {
|
|
21572
|
+
function renderSection(sectionId, title, categories) {
|
|
20958
21573
|
const cards = categories.map(
|
|
20959
21574
|
(cat) => `
|
|
20960
21575
|
<div class="gar-area">
|
|
@@ -20966,10 +21581,9 @@ function healthPage(report, metrics) {
|
|
|
20966
21581
|
${cat.items.length > 0 ? `<ul>${cat.items.map((item) => `<li><span class="ref-id">${escapeHtml(item.id)}</span>${escapeHtml(item.detail)}</li>`).join("")}</ul>` : ""}
|
|
20967
21582
|
</div>`
|
|
20968
21583
|
).join("\n");
|
|
20969
|
-
return
|
|
20970
|
-
|
|
20971
|
-
|
|
20972
|
-
`;
|
|
21584
|
+
return collapsibleSection(sectionId, title, `<div class="gar-areas">${cards}</div>`, {
|
|
21585
|
+
titleClass: "health-section-title"
|
|
21586
|
+
});
|
|
20973
21587
|
}
|
|
20974
21588
|
return `
|
|
20975
21589
|
<div class="page-header">
|
|
@@ -20982,35 +21596,43 @@ function healthPage(report, metrics) {
|
|
|
20982
21596
|
<div class="label">Overall: ${escapeHtml(report.overall)}</div>
|
|
20983
21597
|
</div>
|
|
20984
21598
|
|
|
20985
|
-
${renderSection("Completeness", report.completeness)}
|
|
20986
|
-
|
|
20987
|
-
|
|
20988
|
-
|
|
20989
|
-
|
|
20990
|
-
|
|
20991
|
-
|
|
20992
|
-
|
|
20993
|
-
|
|
20994
|
-
|
|
20995
|
-
|
|
20996
|
-
|
|
20997
|
-
|
|
20998
|
-
|
|
20999
|
-
|
|
21000
|
-
|
|
21599
|
+
${renderSection("health-completeness", "Completeness", report.completeness)}
|
|
21600
|
+
|
|
21601
|
+
${collapsibleSection(
|
|
21602
|
+
"health-completeness-overview",
|
|
21603
|
+
"Completeness Overview",
|
|
21604
|
+
buildHealthGauge(
|
|
21605
|
+
metrics ? Object.entries(metrics.completeness).map(([name, cat]) => ({
|
|
21606
|
+
name: name.replace(/\b\w/g, (c) => c.toUpperCase()),
|
|
21607
|
+
complete: cat.complete,
|
|
21608
|
+
total: cat.total
|
|
21609
|
+
})) : report.completeness.map((c) => {
|
|
21610
|
+
const match = c.summary.match(/(\d+)\s*\/\s*(\d+)/);
|
|
21611
|
+
return {
|
|
21612
|
+
name: c.name,
|
|
21613
|
+
complete: match ? parseInt(match[1], 10) : 0,
|
|
21614
|
+
total: match ? parseInt(match[2], 10) : 0
|
|
21615
|
+
};
|
|
21616
|
+
})
|
|
21617
|
+
),
|
|
21618
|
+
{ titleClass: "health-section-title" }
|
|
21001
21619
|
)}
|
|
21002
21620
|
|
|
21003
|
-
${renderSection("Process", report.process)}
|
|
21004
|
-
|
|
21005
|
-
|
|
21006
|
-
|
|
21007
|
-
|
|
21008
|
-
"
|
|
21009
|
-
|
|
21010
|
-
|
|
21011
|
-
|
|
21012
|
-
|
|
21013
|
-
|
|
21621
|
+
${renderSection("health-process", "Process", report.process)}
|
|
21622
|
+
|
|
21623
|
+
${collapsibleSection(
|
|
21624
|
+
"health-process-summary",
|
|
21625
|
+
"Process Summary",
|
|
21626
|
+
metrics ? buildStatusPie("Process Health", {
|
|
21627
|
+
Stale: metrics.process.stale.length,
|
|
21628
|
+
"Aging Actions": metrics.process.agingActions.length,
|
|
21629
|
+
Healthy: Math.max(
|
|
21630
|
+
0,
|
|
21631
|
+
(metrics.completeness ? Object.values(metrics.completeness).reduce((sum, c) => sum + c.total, 0) : 0) - metrics.process.stale.length - metrics.process.agingActions.length
|
|
21632
|
+
)
|
|
21633
|
+
}) : "",
|
|
21634
|
+
{ titleClass: "health-section-title" }
|
|
21635
|
+
)}
|
|
21014
21636
|
`;
|
|
21015
21637
|
}
|
|
21016
21638
|
|
|
@@ -21068,7 +21690,7 @@ function timelinePage(diagrams) {
|
|
|
21068
21690
|
return `
|
|
21069
21691
|
<div class="page-header">
|
|
21070
21692
|
<h2>Project Timeline</h2>
|
|
21071
|
-
<div class="subtitle">
|
|
21693
|
+
<div class="subtitle">Epic timeline across sprints</div>
|
|
21072
21694
|
</div>
|
|
21073
21695
|
|
|
21074
21696
|
${buildTimelineGantt(diagrams)}
|
|
@@ -21096,9 +21718,10 @@ function upcomingPage(data) {
|
|
|
21096
21718
|
const hasActions = data.dueSoonActions.length > 0;
|
|
21097
21719
|
const hasSprintTasks = data.dueSoonSprintTasks.length > 0;
|
|
21098
21720
|
const hasTrending = data.trending.length > 0;
|
|
21099
|
-
const actionsTable = hasActions ?
|
|
21100
|
-
|
|
21101
|
-
|
|
21721
|
+
const actionsTable = hasActions ? collapsibleSection(
|
|
21722
|
+
"upcoming-actions",
|
|
21723
|
+
"Due Soon \u2014 Actions",
|
|
21724
|
+
`<div class="table-wrap">
|
|
21102
21725
|
<table>
|
|
21103
21726
|
<thead>
|
|
21104
21727
|
<tr>
|
|
@@ -21113,7 +21736,7 @@ function upcomingPage(data) {
|
|
|
21113
21736
|
</thead>
|
|
21114
21737
|
<tbody>
|
|
21115
21738
|
${data.dueSoonActions.map(
|
|
21116
|
-
|
|
21739
|
+
(a) => `
|
|
21117
21740
|
<tr class="${urgencyRowClass(a.urgency)}">
|
|
21118
21741
|
<td><a href="/docs/action/${escapeHtml(a.id)}">${escapeHtml(a.id)}</a></td>
|
|
21119
21742
|
<td>${escapeHtml(a.title)}</td>
|
|
@@ -21123,13 +21746,16 @@ function upcomingPage(data) {
|
|
|
21123
21746
|
<td>${urgencyBadge(a.urgency)}</td>
|
|
21124
21747
|
<td>${a.relatedTaskCount > 0 ? a.relatedTaskCount : "\u2014"}</td>
|
|
21125
21748
|
</tr>`
|
|
21126
|
-
|
|
21749
|
+
).join("")}
|
|
21127
21750
|
</tbody>
|
|
21128
21751
|
</table>
|
|
21129
|
-
</div
|
|
21130
|
-
|
|
21131
|
-
|
|
21132
|
-
|
|
21752
|
+
</div>`,
|
|
21753
|
+
{ titleTag: "h3" }
|
|
21754
|
+
) : "";
|
|
21755
|
+
const sprintTasksTable = hasSprintTasks ? collapsibleSection(
|
|
21756
|
+
"upcoming-sprint-tasks",
|
|
21757
|
+
"Due Soon \u2014 Sprint Tasks",
|
|
21758
|
+
`<div class="table-wrap">
|
|
21133
21759
|
<table>
|
|
21134
21760
|
<thead>
|
|
21135
21761
|
<tr>
|
|
@@ -21143,7 +21769,7 @@ function upcomingPage(data) {
|
|
|
21143
21769
|
</thead>
|
|
21144
21770
|
<tbody>
|
|
21145
21771
|
${data.dueSoonSprintTasks.map(
|
|
21146
|
-
|
|
21772
|
+
(t) => `
|
|
21147
21773
|
<tr class="${urgencyRowClass(t.urgency)}">
|
|
21148
21774
|
<td><a href="/docs/task/${escapeHtml(t.id)}">${escapeHtml(t.id)}</a></td>
|
|
21149
21775
|
<td>${escapeHtml(t.title)}</td>
|
|
@@ -21152,13 +21778,16 @@ function upcomingPage(data) {
|
|
|
21152
21778
|
<td>${formatDate(t.sprintEndDate)}</td>
|
|
21153
21779
|
<td>${urgencyBadge(t.urgency)}</td>
|
|
21154
21780
|
</tr>`
|
|
21155
|
-
|
|
21781
|
+
).join("")}
|
|
21156
21782
|
</tbody>
|
|
21157
21783
|
</table>
|
|
21158
|
-
</div
|
|
21159
|
-
|
|
21160
|
-
|
|
21161
|
-
|
|
21784
|
+
</div>`,
|
|
21785
|
+
{ titleTag: "h3" }
|
|
21786
|
+
) : "";
|
|
21787
|
+
const trendingTable = hasTrending ? collapsibleSection(
|
|
21788
|
+
"upcoming-trending",
|
|
21789
|
+
"Trending",
|
|
21790
|
+
`<div class="table-wrap">
|
|
21162
21791
|
<table>
|
|
21163
21792
|
<thead>
|
|
21164
21793
|
<tr>
|
|
@@ -21173,7 +21802,7 @@ function upcomingPage(data) {
|
|
|
21173
21802
|
</thead>
|
|
21174
21803
|
<tbody>
|
|
21175
21804
|
${data.trending.map(
|
|
21176
|
-
|
|
21805
|
+
(t, i) => `
|
|
21177
21806
|
<tr>
|
|
21178
21807
|
<td><span class="trending-rank">${i + 1}</span></td>
|
|
21179
21808
|
<td><a href="/docs/${escapeHtml(t.type)}/${escapeHtml(t.id)}">${escapeHtml(t.id)}</a></td>
|
|
@@ -21183,10 +21812,12 @@ function upcomingPage(data) {
|
|
|
21183
21812
|
<td><span class="trending-score">${t.score}</span></td>
|
|
21184
21813
|
<td>${t.signals.map((s) => `<span class="signal-tag">${escapeHtml(s.factor)} +${s.points}</span>`).join(" ")}</td>
|
|
21185
21814
|
</tr>`
|
|
21186
|
-
|
|
21815
|
+
).join("")}
|
|
21187
21816
|
</tbody>
|
|
21188
21817
|
</table>
|
|
21189
|
-
</div
|
|
21818
|
+
</div>`,
|
|
21819
|
+
{ titleTag: "h3" }
|
|
21820
|
+
) : "";
|
|
21190
21821
|
const emptyState = !hasActions && !hasSprintTasks && !hasTrending ? '<div class="empty"><p>No upcoming items or trending activity found.</p></div>' : "";
|
|
21191
21822
|
return `
|
|
21192
21823
|
<div class="page-header">
|
|
@@ -21200,7 +21831,199 @@ function upcomingPage(data) {
|
|
|
21200
21831
|
`;
|
|
21201
21832
|
}
|
|
21202
21833
|
|
|
21834
|
+
// src/web/templates/pages/sprint-summary.ts
|
|
21835
|
+
function progressBar(pct) {
|
|
21836
|
+
return `<div class="sprint-progress-bar">
|
|
21837
|
+
<div class="sprint-progress-fill" style="width: ${pct}%"></div>
|
|
21838
|
+
<span class="sprint-progress-label">${pct}%</span>
|
|
21839
|
+
</div>`;
|
|
21840
|
+
}
|
|
21841
|
+
function sprintSummaryPage(data, cached2) {
|
|
21842
|
+
if (!data) {
|
|
21843
|
+
return `
|
|
21844
|
+
<div class="page-header">
|
|
21845
|
+
<h2>Sprint Summary</h2>
|
|
21846
|
+
<div class="subtitle">AI-powered sprint narrative</div>
|
|
21847
|
+
</div>
|
|
21848
|
+
<div class="empty">
|
|
21849
|
+
<h3>No Active Sprint</h3>
|
|
21850
|
+
<p>No active sprint found. Create a sprint and set its status to "active" to see the summary.</p>
|
|
21851
|
+
</div>`;
|
|
21852
|
+
}
|
|
21853
|
+
const statsCards = `
|
|
21854
|
+
<div class="cards">
|
|
21855
|
+
<div class="card">
|
|
21856
|
+
<div class="card-label">Completion</div>
|
|
21857
|
+
<div class="card-value">${data.workItems.completionPct}%</div>
|
|
21858
|
+
<div class="card-sub">${data.workItems.done} / ${data.workItems.total} items done</div>
|
|
21859
|
+
</div>
|
|
21860
|
+
<div class="card">
|
|
21861
|
+
<div class="card-label">Days Remaining</div>
|
|
21862
|
+
<div class="card-value">${data.timeline.daysRemaining}</div>
|
|
21863
|
+
<div class="card-sub">${data.timeline.daysElapsed} of ${data.timeline.totalDays} elapsed</div>
|
|
21864
|
+
</div>
|
|
21865
|
+
<div class="card">
|
|
21866
|
+
<div class="card-label">Epics</div>
|
|
21867
|
+
<div class="card-value">${data.linkedEpics.length}</div>
|
|
21868
|
+
<div class="card-sub">linked to sprint</div>
|
|
21869
|
+
</div>
|
|
21870
|
+
<div class="card">
|
|
21871
|
+
<div class="card-label">Blockers</div>
|
|
21872
|
+
<div class="card-value${data.blockers.length > 0 ? " priority-high" : ""}">${data.blockers.length}</div>
|
|
21873
|
+
<div class="card-sub">${data.openActions.length} open actions</div>
|
|
21874
|
+
</div>
|
|
21875
|
+
</div>`;
|
|
21876
|
+
const epicsTable = data.linkedEpics.length > 0 ? collapsibleSection(
|
|
21877
|
+
"ss-epics",
|
|
21878
|
+
"Linked Epics",
|
|
21879
|
+
`<div class="table-wrap">
|
|
21880
|
+
<table>
|
|
21881
|
+
<thead>
|
|
21882
|
+
<tr><th>ID</th><th>Title</th><th>Status</th><th>Tasks</th></tr>
|
|
21883
|
+
</thead>
|
|
21884
|
+
<tbody>
|
|
21885
|
+
${data.linkedEpics.map((e) => `
|
|
21886
|
+
<tr>
|
|
21887
|
+
<td><a href="/docs/epic/${escapeHtml(e.id)}">${escapeHtml(e.id)}</a></td>
|
|
21888
|
+
<td>${escapeHtml(e.title)}</td>
|
|
21889
|
+
<td>${statusBadge(e.status)}</td>
|
|
21890
|
+
<td>${e.tasksDone} / ${e.tasksTotal}</td>
|
|
21891
|
+
</tr>`).join("")}
|
|
21892
|
+
</tbody>
|
|
21893
|
+
</table>
|
|
21894
|
+
</div>`,
|
|
21895
|
+
{ titleTag: "h3" }
|
|
21896
|
+
) : "";
|
|
21897
|
+
const workItemsSection = data.workItems.total > 0 ? collapsibleSection(
|
|
21898
|
+
"ss-work-items",
|
|
21899
|
+
"Work Items",
|
|
21900
|
+
`<div class="table-wrap">
|
|
21901
|
+
<table>
|
|
21902
|
+
<thead>
|
|
21903
|
+
<tr><th>ID</th><th>Title</th><th>Type</th><th>Status</th></tr>
|
|
21904
|
+
</thead>
|
|
21905
|
+
<tbody>
|
|
21906
|
+
${data.workItems.items.map((w) => `
|
|
21907
|
+
<tr>
|
|
21908
|
+
<td><a href="/docs/${escapeHtml(w.type)}/${escapeHtml(w.id)}">${escapeHtml(w.id)}</a></td>
|
|
21909
|
+
<td>${escapeHtml(w.title)}</td>
|
|
21910
|
+
<td>${escapeHtml(typeLabel(w.type))}</td>
|
|
21911
|
+
<td>${statusBadge(w.status)}</td>
|
|
21912
|
+
</tr>`).join("")}
|
|
21913
|
+
</tbody>
|
|
21914
|
+
</table>
|
|
21915
|
+
</div>`,
|
|
21916
|
+
{ titleTag: "h3", defaultCollapsed: true }
|
|
21917
|
+
) : "";
|
|
21918
|
+
const activitySection = data.artifacts.length > 0 ? collapsibleSection(
|
|
21919
|
+
"ss-activity",
|
|
21920
|
+
"Recent Activity",
|
|
21921
|
+
`<div class="table-wrap">
|
|
21922
|
+
<table>
|
|
21923
|
+
<thead>
|
|
21924
|
+
<tr><th>Date</th><th>ID</th><th>Title</th><th>Type</th><th>Action</th></tr>
|
|
21925
|
+
</thead>
|
|
21926
|
+
<tbody>
|
|
21927
|
+
${data.artifacts.slice(0, 15).map((a) => `
|
|
21928
|
+
<tr>
|
|
21929
|
+
<td>${formatDate(a.date)}</td>
|
|
21930
|
+
<td><a href="/docs/${escapeHtml(a.type)}/${escapeHtml(a.id)}">${escapeHtml(a.id)}</a></td>
|
|
21931
|
+
<td>${escapeHtml(a.title)}</td>
|
|
21932
|
+
<td>${escapeHtml(typeLabel(a.type))}</td>
|
|
21933
|
+
<td>${escapeHtml(a.action)}</td>
|
|
21934
|
+
</tr>`).join("")}
|
|
21935
|
+
</tbody>
|
|
21936
|
+
</table>
|
|
21937
|
+
</div>`,
|
|
21938
|
+
{ titleTag: "h3", defaultCollapsed: true }
|
|
21939
|
+
) : "";
|
|
21940
|
+
const meetingsSection = data.meetings.length > 0 ? collapsibleSection(
|
|
21941
|
+
"ss-meetings",
|
|
21942
|
+
`Meetings (${data.meetings.length})`,
|
|
21943
|
+
`<div class="table-wrap">
|
|
21944
|
+
<table>
|
|
21945
|
+
<thead>
|
|
21946
|
+
<tr><th>Date</th><th>ID</th><th>Title</th></tr>
|
|
21947
|
+
</thead>
|
|
21948
|
+
<tbody>
|
|
21949
|
+
${data.meetings.map((m) => `
|
|
21950
|
+
<tr>
|
|
21951
|
+
<td>${formatDate(m.date)}</td>
|
|
21952
|
+
<td><a href="/docs/meeting/${escapeHtml(m.id)}">${escapeHtml(m.id)}</a></td>
|
|
21953
|
+
<td>${escapeHtml(m.title)}</td>
|
|
21954
|
+
</tr>`).join("")}
|
|
21955
|
+
</tbody>
|
|
21956
|
+
</table>
|
|
21957
|
+
</div>`,
|
|
21958
|
+
{ titleTag: "h3", defaultCollapsed: true }
|
|
21959
|
+
) : "";
|
|
21960
|
+
const goalHtml = data.sprint.goal ? `<div class="sprint-goal"><strong>Goal:</strong> ${escapeHtml(data.sprint.goal)}</div>` : "";
|
|
21961
|
+
const dateRange = data.sprint.startDate && data.sprint.endDate ? `<span class="text-dim">${formatDate(data.sprint.startDate)} \u2014 ${formatDate(data.sprint.endDate)}</span>` : "";
|
|
21962
|
+
return `
|
|
21963
|
+
<div class="page-header">
|
|
21964
|
+
<h2>${escapeHtml(data.sprint.id)} \u2014 ${escapeHtml(data.sprint.title)} ${statusBadge(data.sprint.status)}</h2>
|
|
21965
|
+
<div class="subtitle">Sprint Summary ${dateRange}</div>
|
|
21966
|
+
</div>
|
|
21967
|
+
${goalHtml}
|
|
21968
|
+
${progressBar(data.timeline.percentComplete)}
|
|
21969
|
+
${statsCards}
|
|
21970
|
+
${epicsTable}
|
|
21971
|
+
${workItemsSection}
|
|
21972
|
+
${activitySection}
|
|
21973
|
+
${meetingsSection}
|
|
21974
|
+
|
|
21975
|
+
<div class="sprint-ai-section">
|
|
21976
|
+
<h3>AI Summary</h3>
|
|
21977
|
+
${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>`}
|
|
21978
|
+
<button class="sprint-generate-btn" onclick="generateSummary()" id="generate-btn">${cached2 ? "Regenerate" : "Generate AI Summary"}</button>
|
|
21979
|
+
<div id="summary-loading" class="sprint-loading" style="display:none">
|
|
21980
|
+
<div class="sprint-spinner"></div>
|
|
21981
|
+
<span>Generating summary...</span>
|
|
21982
|
+
</div>
|
|
21983
|
+
<div id="summary-error" class="sprint-error" style="display:none"></div>
|
|
21984
|
+
<div id="summary-content" class="detail-content"${cached2 ? "" : ' style="display:none"'}>${cached2 ? cached2.html : ""}</div>
|
|
21985
|
+
</div>
|
|
21986
|
+
|
|
21987
|
+
<script>
|
|
21988
|
+
async function generateSummary() {
|
|
21989
|
+
var btn = document.getElementById('generate-btn');
|
|
21990
|
+
var loading = document.getElementById('summary-loading');
|
|
21991
|
+
var errorEl = document.getElementById('summary-error');
|
|
21992
|
+
var content = document.getElementById('summary-content');
|
|
21993
|
+
|
|
21994
|
+
btn.disabled = true;
|
|
21995
|
+
btn.style.display = 'none';
|
|
21996
|
+
loading.style.display = 'flex';
|
|
21997
|
+
errorEl.style.display = 'none';
|
|
21998
|
+
content.style.display = 'none';
|
|
21999
|
+
|
|
22000
|
+
try {
|
|
22001
|
+
var res = await fetch('/api/sprint-summary', {
|
|
22002
|
+
method: 'POST',
|
|
22003
|
+
headers: { 'Content-Type': 'application/json' },
|
|
22004
|
+
body: JSON.stringify({ sprintId: '${escapeHtml(data.sprint.id)}' })
|
|
22005
|
+
});
|
|
22006
|
+
var json = await res.json();
|
|
22007
|
+
if (!res.ok) throw new Error(json.error || 'Failed to generate summary');
|
|
22008
|
+
loading.style.display = 'none';
|
|
22009
|
+
content.innerHTML = json.html;
|
|
22010
|
+
content.style.display = 'block';
|
|
22011
|
+
btn.textContent = 'Regenerate';
|
|
22012
|
+
btn.style.display = '';
|
|
22013
|
+
btn.disabled = false;
|
|
22014
|
+
} catch (e) {
|
|
22015
|
+
loading.style.display = 'none';
|
|
22016
|
+
errorEl.textContent = e.message;
|
|
22017
|
+
errorEl.style.display = 'block';
|
|
22018
|
+
btn.style.display = '';
|
|
22019
|
+
btn.disabled = false;
|
|
22020
|
+
}
|
|
22021
|
+
}
|
|
22022
|
+
</script>`;
|
|
22023
|
+
}
|
|
22024
|
+
|
|
21203
22025
|
// src/web/router.ts
|
|
22026
|
+
var sprintSummaryCache = /* @__PURE__ */ new Map();
|
|
21204
22027
|
function handleRequest(req, res, store, projectName, navGroups) {
|
|
21205
22028
|
const parsed = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
21206
22029
|
const pathname = parsed.pathname;
|
|
@@ -21246,6 +22069,42 @@ function handleRequest(req, res, store, projectName, navGroups) {
|
|
|
21246
22069
|
respond(res, layout({ title: "Upcoming", activePath: "/upcoming", projectName, navGroups }, body));
|
|
21247
22070
|
return;
|
|
21248
22071
|
}
|
|
22072
|
+
if (pathname === "/sprint-summary" && req.method === "GET") {
|
|
22073
|
+
const sprintId = parsed.searchParams.get("sprint") ?? void 0;
|
|
22074
|
+
const data = getSprintSummaryData(store, sprintId);
|
|
22075
|
+
const cached2 = data ? sprintSummaryCache.get(data.sprint.id) : void 0;
|
|
22076
|
+
const body = sprintSummaryPage(data, cached2 ? { html: cached2.html, generatedAt: cached2.generatedAt } : void 0);
|
|
22077
|
+
respond(res, layout({ title: "Sprint Summary", activePath: "/sprint-summary", projectName, navGroups }, body));
|
|
22078
|
+
return;
|
|
22079
|
+
}
|
|
22080
|
+
if (pathname === "/api/sprint-summary" && req.method === "POST") {
|
|
22081
|
+
let bodyStr = "";
|
|
22082
|
+
req.on("data", (chunk) => {
|
|
22083
|
+
bodyStr += chunk;
|
|
22084
|
+
});
|
|
22085
|
+
req.on("end", async () => {
|
|
22086
|
+
try {
|
|
22087
|
+
const { sprintId } = JSON.parse(bodyStr || "{}");
|
|
22088
|
+
const data = getSprintSummaryData(store, sprintId);
|
|
22089
|
+
if (!data) {
|
|
22090
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
22091
|
+
res.end(JSON.stringify({ error: "Sprint not found" }));
|
|
22092
|
+
return;
|
|
22093
|
+
}
|
|
22094
|
+
const summary = await generateSprintSummary(data);
|
|
22095
|
+
const html = renderMarkdown(summary);
|
|
22096
|
+
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
22097
|
+
sprintSummaryCache.set(data.sprint.id, { html, generatedAt });
|
|
22098
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
22099
|
+
res.end(JSON.stringify({ summary, html, generatedAt }));
|
|
22100
|
+
} catch (err) {
|
|
22101
|
+
console.error("[marvin web] Sprint summary generation error:", err);
|
|
22102
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
22103
|
+
res.end(JSON.stringify({ error: "Failed to generate summary" }));
|
|
22104
|
+
}
|
|
22105
|
+
});
|
|
22106
|
+
return;
|
|
22107
|
+
}
|
|
21249
22108
|
const boardMatch = pathname.match(/^\/board(?:\/([^/]+))?$/);
|
|
21250
22109
|
if (boardMatch) {
|
|
21251
22110
|
const type = boardMatch[1];
|
|
@@ -21489,6 +22348,24 @@ function createWebTools(store, projectName, navGroups) {
|
|
|
21489
22348
|
};
|
|
21490
22349
|
},
|
|
21491
22350
|
{ annotations: { readOnlyHint: true } }
|
|
22351
|
+
),
|
|
22352
|
+
tool22(
|
|
22353
|
+
"get_dashboard_sprint_summary",
|
|
22354
|
+
"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.",
|
|
22355
|
+
{
|
|
22356
|
+
sprint: external_exports.string().optional().describe("Sprint ID (e.g. 'SP-001'). Omit for the active sprint.")
|
|
22357
|
+
},
|
|
22358
|
+
async (args) => {
|
|
22359
|
+
const data = getSprintSummaryData(store, args.sprint);
|
|
22360
|
+
if (!data) {
|
|
22361
|
+
const msg = args.sprint ? `Sprint ${args.sprint} not found.` : "No active sprint found.";
|
|
22362
|
+
return { content: [{ type: "text", text: msg }], isError: true };
|
|
22363
|
+
}
|
|
22364
|
+
return {
|
|
22365
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
22366
|
+
};
|
|
22367
|
+
},
|
|
22368
|
+
{ annotations: { readOnlyHint: true } }
|
|
21492
22369
|
)
|
|
21493
22370
|
];
|
|
21494
22371
|
}
|
|
@@ -21535,7 +22412,7 @@ async function runSkillAction(action, userPrompt, context) {
|
|
|
21535
22412
|
try {
|
|
21536
22413
|
const mcpServer = createMarvinMcpServer(context.store);
|
|
21537
22414
|
const allowedTools = action.allowGovernanceTools !== false ? GOVERNANCE_TOOL_NAMES : [];
|
|
21538
|
-
const conversation =
|
|
22415
|
+
const conversation = query2({
|
|
21539
22416
|
prompt: userPrompt,
|
|
21540
22417
|
options: {
|
|
21541
22418
|
systemPrompt: action.systemPrompt,
|