mrvn-cli 0.4.6 → 0.4.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -1
- package/dist/index.js +836 -26
- package/dist/index.js.map +1 -1
- package/dist/marvin-serve.js +795 -35
- package/dist/marvin-serve.js.map +1 -1
- package/dist/marvin.js +854 -46
- package/dist/marvin.js.map +1 -1
- package/package.json +1 -1
package/dist/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,385 @@ 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 primaryDocs = workItemDocs.filter((d) => d.frontmatter.type !== "contribution");
|
|
15564
|
+
const byStatus = {};
|
|
15565
|
+
const byType = {};
|
|
15566
|
+
let doneCount = 0;
|
|
15567
|
+
let inProgressCount = 0;
|
|
15568
|
+
let openCount = 0;
|
|
15569
|
+
let blockedCount = 0;
|
|
15570
|
+
for (const doc of primaryDocs) {
|
|
15571
|
+
const s = doc.frontmatter.status;
|
|
15572
|
+
byStatus[s] = (byStatus[s] ?? 0) + 1;
|
|
15573
|
+
byType[doc.frontmatter.type] = (byType[doc.frontmatter.type] ?? 0) + 1;
|
|
15574
|
+
if (DONE_STATUSES.has(s)) doneCount++;
|
|
15575
|
+
else if (s === "in-progress") inProgressCount++;
|
|
15576
|
+
else if (s === "blocked") blockedCount++;
|
|
15577
|
+
else openCount++;
|
|
15578
|
+
}
|
|
15579
|
+
const allItemsById = /* @__PURE__ */ new Map();
|
|
15580
|
+
const childrenByParent = /* @__PURE__ */ new Map();
|
|
15581
|
+
const sprintItemIds = new Set(workItemDocs.map((d) => d.frontmatter.id));
|
|
15582
|
+
for (const doc of workItemDocs) {
|
|
15583
|
+
const about = doc.frontmatter.aboutArtifact;
|
|
15584
|
+
const item = {
|
|
15585
|
+
id: doc.frontmatter.id,
|
|
15586
|
+
title: doc.frontmatter.title,
|
|
15587
|
+
type: doc.frontmatter.type,
|
|
15588
|
+
status: doc.frontmatter.status,
|
|
15589
|
+
aboutArtifact: about
|
|
15590
|
+
};
|
|
15591
|
+
allItemsById.set(item.id, item);
|
|
15592
|
+
if (about && sprintItemIds.has(about)) {
|
|
15593
|
+
if (!childrenByParent.has(about)) childrenByParent.set(about, []);
|
|
15594
|
+
childrenByParent.get(about).push(item);
|
|
15595
|
+
}
|
|
15596
|
+
}
|
|
15597
|
+
const itemsWithChildren = /* @__PURE__ */ new Set();
|
|
15598
|
+
for (const [parentId, children] of childrenByParent) {
|
|
15599
|
+
const parent = allItemsById.get(parentId);
|
|
15600
|
+
if (parent) {
|
|
15601
|
+
parent.children = children;
|
|
15602
|
+
for (const child of children) itemsWithChildren.add(child.id);
|
|
15603
|
+
}
|
|
15604
|
+
}
|
|
15605
|
+
for (const item of allItemsById.values()) {
|
|
15606
|
+
if (item.children) {
|
|
15607
|
+
for (const child of item.children) {
|
|
15608
|
+
const grandchildren = childrenByParent.get(child.id);
|
|
15609
|
+
if (grandchildren) {
|
|
15610
|
+
child.children = grandchildren;
|
|
15611
|
+
for (const gc of grandchildren) itemsWithChildren.add(gc.id);
|
|
15612
|
+
}
|
|
15613
|
+
}
|
|
15614
|
+
}
|
|
15615
|
+
}
|
|
15616
|
+
const items = [];
|
|
15617
|
+
for (const doc of workItemDocs) {
|
|
15618
|
+
if (!itemsWithChildren.has(doc.frontmatter.id)) {
|
|
15619
|
+
items.push(allItemsById.get(doc.frontmatter.id));
|
|
15620
|
+
}
|
|
15621
|
+
}
|
|
15622
|
+
const workItems = {
|
|
15623
|
+
total: primaryDocs.length,
|
|
15624
|
+
done: doneCount,
|
|
15625
|
+
inProgress: inProgressCount,
|
|
15626
|
+
open: openCount,
|
|
15627
|
+
blocked: blockedCount,
|
|
15628
|
+
completionPct: primaryDocs.length > 0 ? Math.round(doneCount / primaryDocs.length * 100) : 0,
|
|
15629
|
+
byStatus,
|
|
15630
|
+
byType,
|
|
15631
|
+
items
|
|
15632
|
+
};
|
|
15633
|
+
const meetings = [];
|
|
15634
|
+
if (startDate && endDate) {
|
|
15635
|
+
const meetingDocs = allDocs.filter((d) => d.frontmatter.type === "meeting");
|
|
15636
|
+
for (const m of meetingDocs) {
|
|
15637
|
+
const meetingDate = m.frontmatter.date ?? m.frontmatter.created.slice(0, 10);
|
|
15638
|
+
if (meetingDate >= startDate && meetingDate <= endDate) {
|
|
15639
|
+
meetings.push({
|
|
15640
|
+
id: m.frontmatter.id,
|
|
15641
|
+
title: m.frontmatter.title,
|
|
15642
|
+
date: meetingDate
|
|
15643
|
+
});
|
|
15644
|
+
}
|
|
15645
|
+
}
|
|
15646
|
+
meetings.sort((a, b) => a.date.localeCompare(b.date));
|
|
15647
|
+
}
|
|
15648
|
+
const artifacts = [];
|
|
15649
|
+
if (startDate && endDate) {
|
|
15650
|
+
for (const doc of allDocs) {
|
|
15651
|
+
if (doc.frontmatter.type === "sprint") continue;
|
|
15652
|
+
const created = doc.frontmatter.created.slice(0, 10);
|
|
15653
|
+
const updated = doc.frontmatter.updated.slice(0, 10);
|
|
15654
|
+
if (created >= startDate && created <= endDate) {
|
|
15655
|
+
artifacts.push({
|
|
15656
|
+
id: doc.frontmatter.id,
|
|
15657
|
+
title: doc.frontmatter.title,
|
|
15658
|
+
type: doc.frontmatter.type,
|
|
15659
|
+
action: "created",
|
|
15660
|
+
date: created
|
|
15661
|
+
});
|
|
15662
|
+
} else if (updated >= startDate && updated <= endDate && updated !== created) {
|
|
15663
|
+
artifacts.push({
|
|
15664
|
+
id: doc.frontmatter.id,
|
|
15665
|
+
title: doc.frontmatter.title,
|
|
15666
|
+
type: doc.frontmatter.type,
|
|
15667
|
+
action: "updated",
|
|
15668
|
+
date: updated
|
|
15669
|
+
});
|
|
15670
|
+
}
|
|
15671
|
+
}
|
|
15672
|
+
artifacts.sort((a, b) => b.date.localeCompare(a.date));
|
|
15673
|
+
}
|
|
15674
|
+
const relevantTags = /* @__PURE__ */ new Set([sprintTag, ...linkedEpicIds.map((id) => `epic:${id}`)]);
|
|
15675
|
+
const openActions = allDocs.filter(
|
|
15676
|
+
(d) => d.frontmatter.type === "action" && !DONE_STATUSES.has(d.frontmatter.status) && d.frontmatter.tags?.some((t) => relevantTags.has(t))
|
|
15677
|
+
).map((d) => ({
|
|
15678
|
+
id: d.frontmatter.id,
|
|
15679
|
+
title: d.frontmatter.title,
|
|
15680
|
+
owner: d.frontmatter.owner,
|
|
15681
|
+
dueDate: d.frontmatter.dueDate
|
|
15682
|
+
}));
|
|
15683
|
+
const openQuestions = allDocs.filter(
|
|
15684
|
+
(d) => d.frontmatter.type === "question" && d.frontmatter.status === "open" && d.frontmatter.tags?.some((t) => relevantTags.has(t))
|
|
15685
|
+
).map((d) => ({
|
|
15686
|
+
id: d.frontmatter.id,
|
|
15687
|
+
title: d.frontmatter.title
|
|
15688
|
+
}));
|
|
15689
|
+
const blockers = allDocs.filter(
|
|
15690
|
+
(d) => d.frontmatter.status === "blocked" && d.frontmatter.tags?.includes(sprintTag)
|
|
15691
|
+
).map((d) => ({
|
|
15692
|
+
id: d.frontmatter.id,
|
|
15693
|
+
title: d.frontmatter.title,
|
|
15694
|
+
type: d.frontmatter.type
|
|
15695
|
+
}));
|
|
15696
|
+
const riskBlockers = allDocs.filter(
|
|
15697
|
+
(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)
|
|
15698
|
+
);
|
|
15699
|
+
for (const d of riskBlockers) {
|
|
15700
|
+
blockers.push({
|
|
15701
|
+
id: d.frontmatter.id,
|
|
15702
|
+
title: d.frontmatter.title,
|
|
15703
|
+
type: d.frontmatter.type
|
|
15704
|
+
});
|
|
15705
|
+
}
|
|
15706
|
+
let velocity = null;
|
|
15707
|
+
const currentRate = workItems.completionPct;
|
|
15708
|
+
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 ?? ""));
|
|
15709
|
+
if (completedSprints.length > 0) {
|
|
15710
|
+
const prev = completedSprints[0];
|
|
15711
|
+
const prevTag = `sprint:${prev.frontmatter.id}`;
|
|
15712
|
+
const prevWorkItems = allDocs.filter(
|
|
15713
|
+
(d) => d.frontmatter.type !== "sprint" && d.frontmatter.type !== "epic" && d.frontmatter.type !== "contribution" && d.frontmatter.tags?.includes(prevTag)
|
|
15714
|
+
);
|
|
15715
|
+
const prevDone = prevWorkItems.filter((d) => DONE_STATUSES.has(d.frontmatter.status)).length;
|
|
15716
|
+
const prevRate = prevWorkItems.length > 0 ? Math.round(prevDone / prevWorkItems.length * 100) : 0;
|
|
15717
|
+
velocity = {
|
|
15718
|
+
currentCompletionRate: currentRate,
|
|
15719
|
+
previousSprintRate: prevRate,
|
|
15720
|
+
previousSprintId: prev.frontmatter.id
|
|
15721
|
+
};
|
|
15722
|
+
} else {
|
|
15723
|
+
velocity = { currentCompletionRate: currentRate };
|
|
15724
|
+
}
|
|
15725
|
+
return {
|
|
15726
|
+
sprint: {
|
|
15727
|
+
id: fm.id,
|
|
15728
|
+
title: fm.title,
|
|
15729
|
+
goal: fm.goal,
|
|
15730
|
+
status: fm.status,
|
|
15731
|
+
startDate,
|
|
15732
|
+
endDate
|
|
15733
|
+
},
|
|
15734
|
+
timeline: { daysElapsed, daysRemaining, totalDays, percentComplete },
|
|
15735
|
+
linkedEpics,
|
|
15736
|
+
workItems,
|
|
15737
|
+
meetings,
|
|
15738
|
+
artifacts,
|
|
15739
|
+
openActions,
|
|
15740
|
+
openQuestions,
|
|
15741
|
+
blockers,
|
|
15742
|
+
velocity
|
|
15743
|
+
};
|
|
15744
|
+
}
|
|
15745
|
+
|
|
15746
|
+
// src/reports/sprint-summary/generator.ts
|
|
15747
|
+
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
15748
|
+
async function generateSprintSummary(data) {
|
|
15749
|
+
const prompt = buildPrompt(data);
|
|
15750
|
+
const result = query({
|
|
15751
|
+
prompt,
|
|
15752
|
+
options: {
|
|
15753
|
+
systemPrompt: SYSTEM_PROMPT,
|
|
15754
|
+
maxTurns: 1,
|
|
15755
|
+
tools: [],
|
|
15756
|
+
allowedTools: []
|
|
15757
|
+
}
|
|
15758
|
+
});
|
|
15759
|
+
for await (const msg of result) {
|
|
15760
|
+
if (msg.type === "assistant") {
|
|
15761
|
+
const text = msg.message.content.find(
|
|
15762
|
+
(b) => b.type === "text"
|
|
15763
|
+
);
|
|
15764
|
+
if (text) return text.text;
|
|
15765
|
+
}
|
|
15766
|
+
}
|
|
15767
|
+
return "Unable to generate sprint summary.";
|
|
15768
|
+
}
|
|
15769
|
+
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:
|
|
15770
|
+
|
|
15771
|
+
## Sprint Health
|
|
15772
|
+
One-line verdict on overall sprint health (healthy / at risk / behind).
|
|
15773
|
+
|
|
15774
|
+
## Goal Progress
|
|
15775
|
+
How close the team is to achieving the sprint goal. Reference the goal text and completion metrics.
|
|
15776
|
+
|
|
15777
|
+
## Key Achievements
|
|
15778
|
+
Notable completions, decisions made, meetings held during the sprint. Use bullet points.
|
|
15779
|
+
|
|
15780
|
+
## Current Risks
|
|
15781
|
+
Blockers, overdue items, unresolved questions, items without owners. Use bullet points. If none, say so.
|
|
15782
|
+
|
|
15783
|
+
## Outcome Projection
|
|
15784
|
+
Given the current pace and time remaining, what's the likely outcome? Will the sprint goal be met?
|
|
15785
|
+
|
|
15786
|
+
Be specific \u2014 reference artifact IDs, dates, and numbers from the data. Keep the tone professional but direct.`;
|
|
15787
|
+
function buildPrompt(data) {
|
|
15788
|
+
const sections = [];
|
|
15789
|
+
sections.push(`# Sprint: ${data.sprint.id} \u2014 ${data.sprint.title}`);
|
|
15790
|
+
sections.push(`Status: ${data.sprint.status}`);
|
|
15791
|
+
if (data.sprint.goal) sections.push(`Goal: ${data.sprint.goal}`);
|
|
15792
|
+
if (data.sprint.startDate) sections.push(`Start: ${data.sprint.startDate}`);
|
|
15793
|
+
if (data.sprint.endDate) sections.push(`End: ${data.sprint.endDate}`);
|
|
15794
|
+
sections.push(`
|
|
15795
|
+
## Timeline`);
|
|
15796
|
+
sections.push(`Days elapsed: ${data.timeline.daysElapsed} / ${data.timeline.totalDays}`);
|
|
15797
|
+
sections.push(`Days remaining: ${data.timeline.daysRemaining}`);
|
|
15798
|
+
sections.push(`Timeline progress: ${data.timeline.percentComplete}%`);
|
|
15799
|
+
sections.push(`
|
|
15800
|
+
## Work Items`);
|
|
15801
|
+
sections.push(`Total: ${data.workItems.total}, Done: ${data.workItems.done}, In Progress: ${data.workItems.inProgress}, Open: ${data.workItems.open}, Blocked: ${data.workItems.blocked}`);
|
|
15802
|
+
sections.push(`Completion: ${data.workItems.completionPct}%`);
|
|
15803
|
+
if (Object.keys(data.workItems.byType).length > 0) {
|
|
15804
|
+
sections.push(`By type: ${Object.entries(data.workItems.byType).map(([t, n]) => `${t}: ${n}`).join(", ")}`);
|
|
15805
|
+
}
|
|
15806
|
+
if (data.linkedEpics.length > 0) {
|
|
15807
|
+
sections.push(`
|
|
15808
|
+
## Linked Epics`);
|
|
15809
|
+
for (const e of data.linkedEpics) {
|
|
15810
|
+
sections.push(`- ${e.id}: ${e.title} [${e.status}] \u2014 ${e.tasksDone}/${e.tasksTotal} tasks done`);
|
|
15811
|
+
}
|
|
15812
|
+
}
|
|
15813
|
+
if (data.meetings.length > 0) {
|
|
15814
|
+
sections.push(`
|
|
15815
|
+
## Meetings During Sprint`);
|
|
15816
|
+
for (const m of data.meetings) {
|
|
15817
|
+
sections.push(`- ${m.date}: ${m.id} \u2014 ${m.title}`);
|
|
15818
|
+
}
|
|
15819
|
+
}
|
|
15820
|
+
if (data.artifacts.length > 0) {
|
|
15821
|
+
sections.push(`
|
|
15822
|
+
## Artifacts Created/Updated During Sprint`);
|
|
15823
|
+
for (const a of data.artifacts.slice(0, 20)) {
|
|
15824
|
+
sections.push(`- ${a.date}: ${a.id} (${a.type}) ${a.action} \u2014 ${a.title}`);
|
|
15825
|
+
}
|
|
15826
|
+
if (data.artifacts.length > 20) {
|
|
15827
|
+
sections.push(`... and ${data.artifacts.length - 20} more`);
|
|
15828
|
+
}
|
|
15829
|
+
}
|
|
15830
|
+
if (data.openActions.length > 0) {
|
|
15831
|
+
sections.push(`
|
|
15832
|
+
## Open Actions`);
|
|
15833
|
+
for (const a of data.openActions) {
|
|
15834
|
+
const owner = a.owner ?? "unowned";
|
|
15835
|
+
const due = a.dueDate ?? "no due date";
|
|
15836
|
+
sections.push(`- ${a.id}: ${a.title} (${owner}, ${due})`);
|
|
15837
|
+
}
|
|
15838
|
+
}
|
|
15839
|
+
if (data.openQuestions.length > 0) {
|
|
15840
|
+
sections.push(`
|
|
15841
|
+
## Open Questions`);
|
|
15842
|
+
for (const q of data.openQuestions) {
|
|
15843
|
+
sections.push(`- ${q.id}: ${q.title}`);
|
|
15844
|
+
}
|
|
15845
|
+
}
|
|
15846
|
+
if (data.blockers.length > 0) {
|
|
15847
|
+
sections.push(`
|
|
15848
|
+
## Blockers`);
|
|
15849
|
+
for (const b of data.blockers) {
|
|
15850
|
+
sections.push(`- ${b.id} (${b.type}): ${b.title}`);
|
|
15851
|
+
}
|
|
15852
|
+
}
|
|
15853
|
+
if (data.velocity) {
|
|
15854
|
+
sections.push(`
|
|
15855
|
+
## Velocity`);
|
|
15856
|
+
sections.push(`Current sprint completion rate: ${data.velocity.currentCompletionRate}%`);
|
|
15857
|
+
if (data.velocity.previousSprintRate !== void 0) {
|
|
15858
|
+
sections.push(`Previous sprint (${data.velocity.previousSprintId}): ${data.velocity.previousSprintRate}%`);
|
|
15859
|
+
}
|
|
15860
|
+
}
|
|
15861
|
+
return sections.join("\n");
|
|
15862
|
+
}
|
|
15863
|
+
|
|
15485
15864
|
// src/plugins/builtin/tools/reports.ts
|
|
15486
15865
|
function createReportTools(store) {
|
|
15487
15866
|
return [
|
|
@@ -15773,6 +16152,25 @@ function createReportTools(store) {
|
|
|
15773
16152
|
},
|
|
15774
16153
|
{ annotations: { readOnlyHint: true } }
|
|
15775
16154
|
),
|
|
16155
|
+
tool8(
|
|
16156
|
+
"generate_sprint_summary",
|
|
16157
|
+
"Generate an AI-powered narrative summary of a sprint's progress, health, achievements, risks, and projected outcome",
|
|
16158
|
+
{
|
|
16159
|
+
sprint: external_exports.string().optional().describe("Sprint ID (e.g. 'SP-001'). Omit for the active sprint.")
|
|
16160
|
+
},
|
|
16161
|
+
async (args) => {
|
|
16162
|
+
const data = collectSprintSummaryData(store, args.sprint);
|
|
16163
|
+
if (!data) {
|
|
16164
|
+
const msg = args.sprint ? `Sprint ${args.sprint} not found.` : "No active sprint found.";
|
|
16165
|
+
return { content: [{ type: "text", text: msg }], isError: true };
|
|
16166
|
+
}
|
|
16167
|
+
const summary = await generateSprintSummary(data);
|
|
16168
|
+
return {
|
|
16169
|
+
content: [{ type: "text", text: summary }]
|
|
16170
|
+
};
|
|
16171
|
+
},
|
|
16172
|
+
{ annotations: { readOnlyHint: true } }
|
|
16173
|
+
),
|
|
15776
16174
|
tool8(
|
|
15777
16175
|
"save_report",
|
|
15778
16176
|
"Save a generated report as a persistent document",
|
|
@@ -16205,18 +16603,18 @@ function createContributionTools(store) {
|
|
|
16205
16603
|
content: external_exports.string().describe("Contribution content \u2014 the input from the persona"),
|
|
16206
16604
|
persona: external_exports.string().describe("Persona making the contribution (e.g. 'tech-lead')"),
|
|
16207
16605
|
contributionType: external_exports.string().describe("Type of contribution (e.g. 'action-result', 'risk-finding')"),
|
|
16208
|
-
aboutArtifact: external_exports.string().
|
|
16209
|
-
status: external_exports.string().optional().describe("Status (default: '
|
|
16606
|
+
aboutArtifact: external_exports.string().describe("Artifact ID this contribution relates to (e.g. 'A-001', 'T-003')"),
|
|
16607
|
+
status: external_exports.string().optional().describe("Status (default: 'done')"),
|
|
16210
16608
|
tags: external_exports.array(external_exports.string()).optional().describe("Tags for categorization")
|
|
16211
16609
|
},
|
|
16212
16610
|
async (args) => {
|
|
16213
16611
|
const frontmatter = {
|
|
16214
16612
|
title: args.title,
|
|
16215
|
-
status: args.status ?? "
|
|
16613
|
+
status: args.status ?? "done",
|
|
16216
16614
|
persona: args.persona,
|
|
16217
16615
|
contributionType: args.contributionType
|
|
16218
16616
|
};
|
|
16219
|
-
|
|
16617
|
+
frontmatter.aboutArtifact = args.aboutArtifact;
|
|
16220
16618
|
if (args.tags) frontmatter.tags = args.tags;
|
|
16221
16619
|
const doc = store.create("contribution", frontmatter, args.content);
|
|
16222
16620
|
return {
|
|
@@ -16616,26 +17014,6 @@ function createSprintPlanningTools(store) {
|
|
|
16616
17014
|
|
|
16617
17015
|
// src/plugins/builtin/tools/tasks.ts
|
|
16618
17016
|
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
17017
|
var linkedEpicArray = external_exports.preprocess(
|
|
16640
17018
|
(val) => {
|
|
16641
17019
|
if (typeof val === "string") {
|
|
@@ -16720,6 +17098,7 @@ function createTaskTools(store) {
|
|
|
16720
17098
|
title: external_exports.string().describe("Task title"),
|
|
16721
17099
|
content: external_exports.string().describe("Task description and implementation details"),
|
|
16722
17100
|
linkedEpic: linkedEpicArray.describe("Epic ID(s) to link this task to (e.g. ['E-001'] or ['E-001', 'E-002'])"),
|
|
17101
|
+
aboutArtifact: external_exports.string().optional().describe("Parent artifact this task derives from (e.g. 'A-001')"),
|
|
16723
17102
|
status: external_exports.enum(["backlog", "ready", "in-progress", "review", "done"]).optional().describe("Task status (default: 'backlog')"),
|
|
16724
17103
|
acceptanceCriteria: external_exports.string().optional().describe("Acceptance criteria for the task"),
|
|
16725
17104
|
technicalNotes: external_exports.string().optional().describe("Technical implementation notes"),
|
|
@@ -16745,6 +17124,7 @@ function createTaskTools(store) {
|
|
|
16745
17124
|
linkedEpic: linkedEpics,
|
|
16746
17125
|
tags: [...generateEpicTags(linkedEpics), ...args.tags ?? []]
|
|
16747
17126
|
};
|
|
17127
|
+
if (args.aboutArtifact) frontmatter.aboutArtifact = args.aboutArtifact;
|
|
16748
17128
|
if (args.acceptanceCriteria) frontmatter.acceptanceCriteria = args.acceptanceCriteria;
|
|
16749
17129
|
if (args.technicalNotes) frontmatter.technicalNotes = args.technicalNotes;
|
|
16750
17130
|
if (args.estimatedPoints !== void 0) frontmatter.estimatedPoints = args.estimatedPoints;
|
|
@@ -16768,6 +17148,7 @@ function createTaskTools(store) {
|
|
|
16768
17148
|
{
|
|
16769
17149
|
id: external_exports.string().describe("Task ID to update"),
|
|
16770
17150
|
title: external_exports.string().optional().describe("New title"),
|
|
17151
|
+
aboutArtifact: external_exports.string().optional().describe("Parent artifact this task derives from (e.g. 'A-001')"),
|
|
16771
17152
|
status: external_exports.enum(["backlog", "ready", "in-progress", "review", "done"]).optional().describe("New status"),
|
|
16772
17153
|
content: external_exports.string().optional().describe("New content"),
|
|
16773
17154
|
linkedEpic: linkedEpicArray.optional().describe("New linked epic ID(s)"),
|
|
@@ -19020,7 +19401,7 @@ ${fragment}`);
|
|
|
19020
19401
|
import { tool as tool23 } from "@anthropic-ai/claude-agent-sdk";
|
|
19021
19402
|
|
|
19022
19403
|
// src/skills/action-runner.ts
|
|
19023
|
-
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
19404
|
+
import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
|
|
19024
19405
|
|
|
19025
19406
|
// src/agent/mcp-server.ts
|
|
19026
19407
|
import {
|
|
@@ -19152,7 +19533,7 @@ function computeUrgency(dueDateStr, todayStr) {
|
|
|
19152
19533
|
if (diffDays <= 14) return "upcoming";
|
|
19153
19534
|
return "later";
|
|
19154
19535
|
}
|
|
19155
|
-
var
|
|
19536
|
+
var DONE_STATUSES2 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
19156
19537
|
function getUpcomingData(store) {
|
|
19157
19538
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
19158
19539
|
const allDocs = store.list();
|
|
@@ -19161,7 +19542,7 @@ function getUpcomingData(store) {
|
|
|
19161
19542
|
docById.set(doc.frontmatter.id, doc);
|
|
19162
19543
|
}
|
|
19163
19544
|
const actions = allDocs.filter(
|
|
19164
|
-
(d) => d.frontmatter.type === "action" && !
|
|
19545
|
+
(d) => d.frontmatter.type === "action" && !DONE_STATUSES2.has(d.frontmatter.status)
|
|
19165
19546
|
);
|
|
19166
19547
|
const actionsWithDue = actions.filter((d) => d.frontmatter.dueDate);
|
|
19167
19548
|
const sprints = allDocs.filter((d) => d.frontmatter.type === "sprint");
|
|
@@ -19225,7 +19606,7 @@ function getUpcomingData(store) {
|
|
|
19225
19606
|
const sprintEnd = sprint.frontmatter.endDate;
|
|
19226
19607
|
const sprintTaskDocs = getSprintTasks(sprint);
|
|
19227
19608
|
for (const task of sprintTaskDocs) {
|
|
19228
|
-
if (
|
|
19609
|
+
if (DONE_STATUSES2.has(task.frontmatter.status)) continue;
|
|
19229
19610
|
const existing = taskSprintMap.get(task.frontmatter.id);
|
|
19230
19611
|
if (!existing || sprintEnd < existing.sprintEnd) {
|
|
19231
19612
|
taskSprintMap.set(task.frontmatter.id, { task, sprint, sprintEnd });
|
|
@@ -19242,7 +19623,7 @@ function getUpcomingData(store) {
|
|
|
19242
19623
|
urgency: computeUrgency(sprintEnd, today)
|
|
19243
19624
|
})).sort((a, b) => a.sprintEndDate.localeCompare(b.sprintEndDate));
|
|
19244
19625
|
const openItems = allDocs.filter(
|
|
19245
|
-
(d) => ["action", "question", "task"].includes(d.frontmatter.type) && !
|
|
19626
|
+
(d) => ["action", "question", "task"].includes(d.frontmatter.type) && !DONE_STATUSES2.has(d.frontmatter.status)
|
|
19246
19627
|
);
|
|
19247
19628
|
const fourteenDaysAgo = new Date(todayMs - fourteenDaysMs).toISOString().slice(0, 10);
|
|
19248
19629
|
const recentMeetings = allDocs.filter(
|
|
@@ -19340,6 +19721,9 @@ function getUpcomingData(store) {
|
|
|
19340
19721
|
}).filter((item) => item.score > 0).sort((a, b) => b.score - a.score).slice(0, 15);
|
|
19341
19722
|
return { dueSoonActions, dueSoonSprintTasks, trending };
|
|
19342
19723
|
}
|
|
19724
|
+
function getSprintSummaryData(store, sprintId) {
|
|
19725
|
+
return collectSprintSummaryData(store, sprintId);
|
|
19726
|
+
}
|
|
19343
19727
|
|
|
19344
19728
|
// src/web/templates/layout.ts
|
|
19345
19729
|
function collapsibleSection(sectionId, title, content, opts) {
|
|
@@ -19481,6 +19865,7 @@ function layout(opts, body) {
|
|
|
19481
19865
|
const topItems = [
|
|
19482
19866
|
{ href: "/", label: "Overview" },
|
|
19483
19867
|
{ href: "/upcoming", label: "Upcoming" },
|
|
19868
|
+
{ href: "/sprint-summary", label: "Sprint Summary" },
|
|
19484
19869
|
{ href: "/timeline", label: "Timeline" },
|
|
19485
19870
|
{ href: "/board", label: "Board" },
|
|
19486
19871
|
{ href: "/gar", label: "GAR Report" },
|
|
@@ -19864,6 +20249,17 @@ tr:hover td {
|
|
|
19864
20249
|
background: var(--bg-hover);
|
|
19865
20250
|
}
|
|
19866
20251
|
|
|
20252
|
+
/* Hierarchical work-item sub-rows */
|
|
20253
|
+
.child-row td {
|
|
20254
|
+
font-size: 0.8125rem;
|
|
20255
|
+
border-bottom-style: dashed;
|
|
20256
|
+
}
|
|
20257
|
+
.contribution-row td {
|
|
20258
|
+
font-size: 0.8125rem;
|
|
20259
|
+
color: var(--text-dim);
|
|
20260
|
+
border-bottom-style: dashed;
|
|
20261
|
+
}
|
|
20262
|
+
|
|
19867
20263
|
/* GAR */
|
|
19868
20264
|
.gar-overall {
|
|
19869
20265
|
text-align: center;
|
|
@@ -20491,6 +20887,112 @@ tr:hover td {
|
|
|
20491
20887
|
|
|
20492
20888
|
.text-dim { color: var(--text-dim); }
|
|
20493
20889
|
|
|
20890
|
+
/* Sprint Summary */
|
|
20891
|
+
.sprint-goal {
|
|
20892
|
+
background: var(--bg-card);
|
|
20893
|
+
border: 1px solid var(--border);
|
|
20894
|
+
border-radius: var(--radius);
|
|
20895
|
+
padding: 0.75rem 1rem;
|
|
20896
|
+
margin-bottom: 1rem;
|
|
20897
|
+
font-size: 0.9rem;
|
|
20898
|
+
color: var(--text);
|
|
20899
|
+
}
|
|
20900
|
+
|
|
20901
|
+
.sprint-progress-bar {
|
|
20902
|
+
position: relative;
|
|
20903
|
+
height: 24px;
|
|
20904
|
+
background: var(--bg-card);
|
|
20905
|
+
border: 1px solid var(--border);
|
|
20906
|
+
border-radius: 12px;
|
|
20907
|
+
margin-bottom: 1.25rem;
|
|
20908
|
+
overflow: hidden;
|
|
20909
|
+
}
|
|
20910
|
+
|
|
20911
|
+
.sprint-progress-fill {
|
|
20912
|
+
height: 100%;
|
|
20913
|
+
background: linear-gradient(90deg, var(--accent-dim), var(--accent));
|
|
20914
|
+
border-radius: 12px;
|
|
20915
|
+
transition: width 0.3s ease;
|
|
20916
|
+
}
|
|
20917
|
+
|
|
20918
|
+
.sprint-progress-label {
|
|
20919
|
+
position: absolute;
|
|
20920
|
+
top: 50%;
|
|
20921
|
+
left: 50%;
|
|
20922
|
+
transform: translate(-50%, -50%);
|
|
20923
|
+
font-size: 0.7rem;
|
|
20924
|
+
font-weight: 700;
|
|
20925
|
+
color: var(--text);
|
|
20926
|
+
}
|
|
20927
|
+
|
|
20928
|
+
.sprint-ai-section {
|
|
20929
|
+
margin-top: 2rem;
|
|
20930
|
+
background: var(--bg-card);
|
|
20931
|
+
border: 1px solid var(--border);
|
|
20932
|
+
border-radius: var(--radius);
|
|
20933
|
+
padding: 1.5rem;
|
|
20934
|
+
}
|
|
20935
|
+
|
|
20936
|
+
.sprint-ai-section h3 {
|
|
20937
|
+
font-size: 1rem;
|
|
20938
|
+
font-weight: 600;
|
|
20939
|
+
margin-bottom: 0.5rem;
|
|
20940
|
+
}
|
|
20941
|
+
|
|
20942
|
+
.sprint-generate-btn {
|
|
20943
|
+
background: var(--accent);
|
|
20944
|
+
color: #fff;
|
|
20945
|
+
border: none;
|
|
20946
|
+
border-radius: var(--radius);
|
|
20947
|
+
padding: 0.5rem 1.25rem;
|
|
20948
|
+
font-size: 0.85rem;
|
|
20949
|
+
font-weight: 600;
|
|
20950
|
+
cursor: pointer;
|
|
20951
|
+
margin-top: 0.75rem;
|
|
20952
|
+
transition: background 0.15s;
|
|
20953
|
+
}
|
|
20954
|
+
|
|
20955
|
+
.sprint-generate-btn:hover:not(:disabled) {
|
|
20956
|
+
background: var(--accent-dim);
|
|
20957
|
+
}
|
|
20958
|
+
|
|
20959
|
+
.sprint-generate-btn:disabled {
|
|
20960
|
+
opacity: 0.5;
|
|
20961
|
+
cursor: not-allowed;
|
|
20962
|
+
}
|
|
20963
|
+
|
|
20964
|
+
.sprint-loading {
|
|
20965
|
+
display: flex;
|
|
20966
|
+
align-items: center;
|
|
20967
|
+
gap: 0.75rem;
|
|
20968
|
+
padding: 1rem 0;
|
|
20969
|
+
color: var(--text-dim);
|
|
20970
|
+
font-size: 0.85rem;
|
|
20971
|
+
}
|
|
20972
|
+
|
|
20973
|
+
.sprint-spinner {
|
|
20974
|
+
width: 20px;
|
|
20975
|
+
height: 20px;
|
|
20976
|
+
border: 2px solid var(--border);
|
|
20977
|
+
border-top-color: var(--accent);
|
|
20978
|
+
border-radius: 50%;
|
|
20979
|
+
animation: sprint-spin 0.8s linear infinite;
|
|
20980
|
+
}
|
|
20981
|
+
|
|
20982
|
+
@keyframes sprint-spin {
|
|
20983
|
+
to { transform: rotate(360deg); }
|
|
20984
|
+
}
|
|
20985
|
+
|
|
20986
|
+
.sprint-error {
|
|
20987
|
+
color: var(--red);
|
|
20988
|
+
font-size: 0.85rem;
|
|
20989
|
+
padding: 0.5rem 0;
|
|
20990
|
+
}
|
|
20991
|
+
|
|
20992
|
+
.sprint-ai-section .detail-content {
|
|
20993
|
+
margin-top: 1rem;
|
|
20994
|
+
}
|
|
20995
|
+
|
|
20494
20996
|
/* Collapsible sections */
|
|
20495
20997
|
.collapsible-header {
|
|
20496
20998
|
cursor: pointer;
|
|
@@ -21382,7 +21884,211 @@ function upcomingPage(data) {
|
|
|
21382
21884
|
`;
|
|
21383
21885
|
}
|
|
21384
21886
|
|
|
21887
|
+
// src/web/templates/pages/sprint-summary.ts
|
|
21888
|
+
function progressBar(pct) {
|
|
21889
|
+
return `<div class="sprint-progress-bar">
|
|
21890
|
+
<div class="sprint-progress-fill" style="width: ${pct}%"></div>
|
|
21891
|
+
<span class="sprint-progress-label">${pct}%</span>
|
|
21892
|
+
</div>`;
|
|
21893
|
+
}
|
|
21894
|
+
function sprintSummaryPage(data, cached2) {
|
|
21895
|
+
if (!data) {
|
|
21896
|
+
return `
|
|
21897
|
+
<div class="page-header">
|
|
21898
|
+
<h2>Sprint Summary</h2>
|
|
21899
|
+
<div class="subtitle">AI-powered sprint narrative</div>
|
|
21900
|
+
</div>
|
|
21901
|
+
<div class="empty">
|
|
21902
|
+
<h3>No Active Sprint</h3>
|
|
21903
|
+
<p>No active sprint found. Create a sprint and set its status to "active" to see the summary.</p>
|
|
21904
|
+
</div>`;
|
|
21905
|
+
}
|
|
21906
|
+
const statsCards = `
|
|
21907
|
+
<div class="cards">
|
|
21908
|
+
<div class="card">
|
|
21909
|
+
<div class="card-label">Completion</div>
|
|
21910
|
+
<div class="card-value">${data.workItems.completionPct}%</div>
|
|
21911
|
+
<div class="card-sub">${data.workItems.done} / ${data.workItems.total} items done</div>
|
|
21912
|
+
</div>
|
|
21913
|
+
<div class="card">
|
|
21914
|
+
<div class="card-label">Days Remaining</div>
|
|
21915
|
+
<div class="card-value">${data.timeline.daysRemaining}</div>
|
|
21916
|
+
<div class="card-sub">${data.timeline.daysElapsed} of ${data.timeline.totalDays} elapsed</div>
|
|
21917
|
+
</div>
|
|
21918
|
+
<div class="card">
|
|
21919
|
+
<div class="card-label">Epics</div>
|
|
21920
|
+
<div class="card-value">${data.linkedEpics.length}</div>
|
|
21921
|
+
<div class="card-sub">linked to sprint</div>
|
|
21922
|
+
</div>
|
|
21923
|
+
<div class="card">
|
|
21924
|
+
<div class="card-label">Blockers</div>
|
|
21925
|
+
<div class="card-value${data.blockers.length > 0 ? " priority-high" : ""}">${data.blockers.length}</div>
|
|
21926
|
+
<div class="card-sub">${data.openActions.length} open actions</div>
|
|
21927
|
+
</div>
|
|
21928
|
+
</div>`;
|
|
21929
|
+
const epicsTable = data.linkedEpics.length > 0 ? collapsibleSection(
|
|
21930
|
+
"ss-epics",
|
|
21931
|
+
"Linked Epics",
|
|
21932
|
+
`<div class="table-wrap">
|
|
21933
|
+
<table>
|
|
21934
|
+
<thead>
|
|
21935
|
+
<tr><th>ID</th><th>Title</th><th>Status</th><th>Tasks</th></tr>
|
|
21936
|
+
</thead>
|
|
21937
|
+
<tbody>
|
|
21938
|
+
${data.linkedEpics.map((e) => `
|
|
21939
|
+
<tr>
|
|
21940
|
+
<td><a href="/docs/epic/${escapeHtml(e.id)}">${escapeHtml(e.id)}</a></td>
|
|
21941
|
+
<td>${escapeHtml(e.title)}</td>
|
|
21942
|
+
<td>${statusBadge(e.status)}</td>
|
|
21943
|
+
<td>${e.tasksDone} / ${e.tasksTotal}</td>
|
|
21944
|
+
</tr>`).join("")}
|
|
21945
|
+
</tbody>
|
|
21946
|
+
</table>
|
|
21947
|
+
</div>`,
|
|
21948
|
+
{ titleTag: "h3" }
|
|
21949
|
+
) : "";
|
|
21950
|
+
function renderItemRows(items, depth = 0) {
|
|
21951
|
+
return items.flatMap((w) => {
|
|
21952
|
+
const isChild = depth > 0;
|
|
21953
|
+
const isContribution = w.type === "contribution";
|
|
21954
|
+
const rowClass = isContribution ? ' class="contribution-row"' : isChild ? ' class="child-row"' : "";
|
|
21955
|
+
const indent = depth > 0 ? ` style="padding-left: ${0.75 + depth * 1}rem"` : "";
|
|
21956
|
+
const row = `
|
|
21957
|
+
<tr${rowClass}>
|
|
21958
|
+
<td${indent}><a href="/docs/${escapeHtml(w.type)}/${escapeHtml(w.id)}">${escapeHtml(w.id)}</a></td>
|
|
21959
|
+
<td>${escapeHtml(w.title)}</td>
|
|
21960
|
+
<td>${escapeHtml(typeLabel(w.type))}</td>
|
|
21961
|
+
<td>${statusBadge(w.status)}</td>
|
|
21962
|
+
</tr>`;
|
|
21963
|
+
const childRows = w.children ? renderItemRows(w.children, depth + 1) : [];
|
|
21964
|
+
return [row, ...childRows];
|
|
21965
|
+
});
|
|
21966
|
+
}
|
|
21967
|
+
const workItemRows = renderItemRows(data.workItems.items);
|
|
21968
|
+
const workItemsSection = workItemRows.length > 0 ? collapsibleSection(
|
|
21969
|
+
"ss-work-items",
|
|
21970
|
+
"Work Items",
|
|
21971
|
+
`<div class="table-wrap">
|
|
21972
|
+
<table>
|
|
21973
|
+
<thead>
|
|
21974
|
+
<tr><th>ID</th><th>Title</th><th>Type</th><th>Status</th></tr>
|
|
21975
|
+
</thead>
|
|
21976
|
+
<tbody>
|
|
21977
|
+
${workItemRows.join("")}
|
|
21978
|
+
</tbody>
|
|
21979
|
+
</table>
|
|
21980
|
+
</div>`,
|
|
21981
|
+
{ titleTag: "h3", defaultCollapsed: true }
|
|
21982
|
+
) : "";
|
|
21983
|
+
const activitySection = data.artifacts.length > 0 ? collapsibleSection(
|
|
21984
|
+
"ss-activity",
|
|
21985
|
+
"Recent Activity",
|
|
21986
|
+
`<div class="table-wrap">
|
|
21987
|
+
<table>
|
|
21988
|
+
<thead>
|
|
21989
|
+
<tr><th>Date</th><th>ID</th><th>Title</th><th>Type</th><th>Action</th></tr>
|
|
21990
|
+
</thead>
|
|
21991
|
+
<tbody>
|
|
21992
|
+
${data.artifacts.slice(0, 15).map((a) => `
|
|
21993
|
+
<tr>
|
|
21994
|
+
<td>${formatDate(a.date)}</td>
|
|
21995
|
+
<td><a href="/docs/${escapeHtml(a.type)}/${escapeHtml(a.id)}">${escapeHtml(a.id)}</a></td>
|
|
21996
|
+
<td>${escapeHtml(a.title)}</td>
|
|
21997
|
+
<td>${escapeHtml(typeLabel(a.type))}</td>
|
|
21998
|
+
<td>${escapeHtml(a.action)}</td>
|
|
21999
|
+
</tr>`).join("")}
|
|
22000
|
+
</tbody>
|
|
22001
|
+
</table>
|
|
22002
|
+
</div>`,
|
|
22003
|
+
{ titleTag: "h3", defaultCollapsed: true }
|
|
22004
|
+
) : "";
|
|
22005
|
+
const meetingsSection = data.meetings.length > 0 ? collapsibleSection(
|
|
22006
|
+
"ss-meetings",
|
|
22007
|
+
`Meetings (${data.meetings.length})`,
|
|
22008
|
+
`<div class="table-wrap">
|
|
22009
|
+
<table>
|
|
22010
|
+
<thead>
|
|
22011
|
+
<tr><th>Date</th><th>ID</th><th>Title</th></tr>
|
|
22012
|
+
</thead>
|
|
22013
|
+
<tbody>
|
|
22014
|
+
${data.meetings.map((m) => `
|
|
22015
|
+
<tr>
|
|
22016
|
+
<td>${formatDate(m.date)}</td>
|
|
22017
|
+
<td><a href="/docs/meeting/${escapeHtml(m.id)}">${escapeHtml(m.id)}</a></td>
|
|
22018
|
+
<td>${escapeHtml(m.title)}</td>
|
|
22019
|
+
</tr>`).join("")}
|
|
22020
|
+
</tbody>
|
|
22021
|
+
</table>
|
|
22022
|
+
</div>`,
|
|
22023
|
+
{ titleTag: "h3", defaultCollapsed: true }
|
|
22024
|
+
) : "";
|
|
22025
|
+
const goalHtml = data.sprint.goal ? `<div class="sprint-goal"><strong>Goal:</strong> ${escapeHtml(data.sprint.goal)}</div>` : "";
|
|
22026
|
+
const dateRange = data.sprint.startDate && data.sprint.endDate ? `<span class="text-dim">${formatDate(data.sprint.startDate)} \u2014 ${formatDate(data.sprint.endDate)}</span>` : "";
|
|
22027
|
+
return `
|
|
22028
|
+
<div class="page-header">
|
|
22029
|
+
<h2>${escapeHtml(data.sprint.id)} \u2014 ${escapeHtml(data.sprint.title)} ${statusBadge(data.sprint.status)}</h2>
|
|
22030
|
+
<div class="subtitle">Sprint Summary ${dateRange}</div>
|
|
22031
|
+
</div>
|
|
22032
|
+
${goalHtml}
|
|
22033
|
+
${progressBar(data.timeline.percentComplete)}
|
|
22034
|
+
${statsCards}
|
|
22035
|
+
${epicsTable}
|
|
22036
|
+
${workItemsSection}
|
|
22037
|
+
${activitySection}
|
|
22038
|
+
${meetingsSection}
|
|
22039
|
+
|
|
22040
|
+
<div class="sprint-ai-section">
|
|
22041
|
+
<h3>AI Summary</h3>
|
|
22042
|
+
${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>`}
|
|
22043
|
+
<button class="sprint-generate-btn" onclick="generateSummary()" id="generate-btn">${cached2 ? "Regenerate" : "Generate AI Summary"}</button>
|
|
22044
|
+
<div id="summary-loading" class="sprint-loading" style="display:none">
|
|
22045
|
+
<div class="sprint-spinner"></div>
|
|
22046
|
+
<span>Generating summary...</span>
|
|
22047
|
+
</div>
|
|
22048
|
+
<div id="summary-error" class="sprint-error" style="display:none"></div>
|
|
22049
|
+
<div id="summary-content" class="detail-content"${cached2 ? "" : ' style="display:none"'}>${cached2 ? cached2.html : ""}</div>
|
|
22050
|
+
</div>
|
|
22051
|
+
|
|
22052
|
+
<script>
|
|
22053
|
+
async function generateSummary() {
|
|
22054
|
+
var btn = document.getElementById('generate-btn');
|
|
22055
|
+
var loading = document.getElementById('summary-loading');
|
|
22056
|
+
var errorEl = document.getElementById('summary-error');
|
|
22057
|
+
var content = document.getElementById('summary-content');
|
|
22058
|
+
|
|
22059
|
+
btn.disabled = true;
|
|
22060
|
+
btn.style.display = 'none';
|
|
22061
|
+
loading.style.display = 'flex';
|
|
22062
|
+
errorEl.style.display = 'none';
|
|
22063
|
+
content.style.display = 'none';
|
|
22064
|
+
|
|
22065
|
+
try {
|
|
22066
|
+
var res = await fetch('/api/sprint-summary', {
|
|
22067
|
+
method: 'POST',
|
|
22068
|
+
headers: { 'Content-Type': 'application/json' },
|
|
22069
|
+
body: JSON.stringify({ sprintId: '${escapeHtml(data.sprint.id)}' })
|
|
22070
|
+
});
|
|
22071
|
+
var json = await res.json();
|
|
22072
|
+
if (!res.ok) throw new Error(json.error || 'Failed to generate summary');
|
|
22073
|
+
loading.style.display = 'none';
|
|
22074
|
+
content.innerHTML = json.html;
|
|
22075
|
+
content.style.display = 'block';
|
|
22076
|
+
btn.textContent = 'Regenerate';
|
|
22077
|
+
btn.style.display = '';
|
|
22078
|
+
btn.disabled = false;
|
|
22079
|
+
} catch (e) {
|
|
22080
|
+
loading.style.display = 'none';
|
|
22081
|
+
errorEl.textContent = e.message;
|
|
22082
|
+
errorEl.style.display = 'block';
|
|
22083
|
+
btn.style.display = '';
|
|
22084
|
+
btn.disabled = false;
|
|
22085
|
+
}
|
|
22086
|
+
}
|
|
22087
|
+
</script>`;
|
|
22088
|
+
}
|
|
22089
|
+
|
|
21385
22090
|
// src/web/router.ts
|
|
22091
|
+
var sprintSummaryCache = /* @__PURE__ */ new Map();
|
|
21386
22092
|
function handleRequest(req, res, store, projectName, navGroups) {
|
|
21387
22093
|
const parsed = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
21388
22094
|
const pathname = parsed.pathname;
|
|
@@ -21428,6 +22134,42 @@ function handleRequest(req, res, store, projectName, navGroups) {
|
|
|
21428
22134
|
respond(res, layout({ title: "Upcoming", activePath: "/upcoming", projectName, navGroups }, body));
|
|
21429
22135
|
return;
|
|
21430
22136
|
}
|
|
22137
|
+
if (pathname === "/sprint-summary" && req.method === "GET") {
|
|
22138
|
+
const sprintId = parsed.searchParams.get("sprint") ?? void 0;
|
|
22139
|
+
const data = getSprintSummaryData(store, sprintId);
|
|
22140
|
+
const cached2 = data ? sprintSummaryCache.get(data.sprint.id) : void 0;
|
|
22141
|
+
const body = sprintSummaryPage(data, cached2 ? { html: cached2.html, generatedAt: cached2.generatedAt } : void 0);
|
|
22142
|
+
respond(res, layout({ title: "Sprint Summary", activePath: "/sprint-summary", projectName, navGroups }, body));
|
|
22143
|
+
return;
|
|
22144
|
+
}
|
|
22145
|
+
if (pathname === "/api/sprint-summary" && req.method === "POST") {
|
|
22146
|
+
let bodyStr = "";
|
|
22147
|
+
req.on("data", (chunk) => {
|
|
22148
|
+
bodyStr += chunk;
|
|
22149
|
+
});
|
|
22150
|
+
req.on("end", async () => {
|
|
22151
|
+
try {
|
|
22152
|
+
const { sprintId } = JSON.parse(bodyStr || "{}");
|
|
22153
|
+
const data = getSprintSummaryData(store, sprintId);
|
|
22154
|
+
if (!data) {
|
|
22155
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
22156
|
+
res.end(JSON.stringify({ error: "Sprint not found" }));
|
|
22157
|
+
return;
|
|
22158
|
+
}
|
|
22159
|
+
const summary = await generateSprintSummary(data);
|
|
22160
|
+
const html = renderMarkdown(summary);
|
|
22161
|
+
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
22162
|
+
sprintSummaryCache.set(data.sprint.id, { html, generatedAt });
|
|
22163
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
22164
|
+
res.end(JSON.stringify({ summary, html, generatedAt }));
|
|
22165
|
+
} catch (err) {
|
|
22166
|
+
console.error("[marvin web] Sprint summary generation error:", err);
|
|
22167
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
22168
|
+
res.end(JSON.stringify({ error: "Failed to generate summary" }));
|
|
22169
|
+
}
|
|
22170
|
+
});
|
|
22171
|
+
return;
|
|
22172
|
+
}
|
|
21431
22173
|
const boardMatch = pathname.match(/^\/board(?:\/([^/]+))?$/);
|
|
21432
22174
|
if (boardMatch) {
|
|
21433
22175
|
const type = boardMatch[1];
|
|
@@ -21671,6 +22413,24 @@ function createWebTools(store, projectName, navGroups) {
|
|
|
21671
22413
|
};
|
|
21672
22414
|
},
|
|
21673
22415
|
{ annotations: { readOnlyHint: true } }
|
|
22416
|
+
),
|
|
22417
|
+
tool22(
|
|
22418
|
+
"get_dashboard_sprint_summary",
|
|
22419
|
+
"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.",
|
|
22420
|
+
{
|
|
22421
|
+
sprint: external_exports.string().optional().describe("Sprint ID (e.g. 'SP-001'). Omit for the active sprint.")
|
|
22422
|
+
},
|
|
22423
|
+
async (args) => {
|
|
22424
|
+
const data = getSprintSummaryData(store, args.sprint);
|
|
22425
|
+
if (!data) {
|
|
22426
|
+
const msg = args.sprint ? `Sprint ${args.sprint} not found.` : "No active sprint found.";
|
|
22427
|
+
return { content: [{ type: "text", text: msg }], isError: true };
|
|
22428
|
+
}
|
|
22429
|
+
return {
|
|
22430
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
22431
|
+
};
|
|
22432
|
+
},
|
|
22433
|
+
{ annotations: { readOnlyHint: true } }
|
|
21674
22434
|
)
|
|
21675
22435
|
];
|
|
21676
22436
|
}
|
|
@@ -21717,7 +22477,7 @@ async function runSkillAction(action, userPrompt, context) {
|
|
|
21717
22477
|
try {
|
|
21718
22478
|
const mcpServer = createMarvinMcpServer(context.store);
|
|
21719
22479
|
const allowedTools = action.allowGovernanceTools !== false ? GOVERNANCE_TOOL_NAMES : [];
|
|
21720
|
-
const conversation =
|
|
22480
|
+
const conversation = query2({
|
|
21721
22481
|
prompt: userPrompt,
|
|
21722
22482
|
options: {
|
|
21723
22483
|
systemPrompt: action.systemPrompt,
|