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.js
CHANGED
|
@@ -14495,6 +14495,346 @@ function evaluateHealth(projectName, metrics) {
|
|
|
14495
14495
|
};
|
|
14496
14496
|
}
|
|
14497
14497
|
|
|
14498
|
+
// src/plugins/builtin/tools/task-utils.ts
|
|
14499
|
+
function normalizeLinkedEpics(value) {
|
|
14500
|
+
if (value === void 0 || value === null) return [];
|
|
14501
|
+
if (typeof value === "string") {
|
|
14502
|
+
try {
|
|
14503
|
+
const parsed = JSON.parse(value);
|
|
14504
|
+
if (Array.isArray(parsed)) return parsed.filter((v) => typeof v === "string");
|
|
14505
|
+
} catch {
|
|
14506
|
+
}
|
|
14507
|
+
return [value];
|
|
14508
|
+
}
|
|
14509
|
+
if (Array.isArray(value)) return value.filter((v) => typeof v === "string");
|
|
14510
|
+
return [];
|
|
14511
|
+
}
|
|
14512
|
+
function generateEpicTags(epics) {
|
|
14513
|
+
return epics.map((id) => `epic:${id}`);
|
|
14514
|
+
}
|
|
14515
|
+
|
|
14516
|
+
// src/reports/sprint-summary/collector.ts
|
|
14517
|
+
var DONE_STATUSES = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
14518
|
+
function collectSprintSummaryData(store, sprintId) {
|
|
14519
|
+
const allDocs = store.list();
|
|
14520
|
+
const sprintDocs = allDocs.filter((d) => d.frontmatter.type === "sprint");
|
|
14521
|
+
let sprintDoc;
|
|
14522
|
+
if (sprintId) {
|
|
14523
|
+
sprintDoc = sprintDocs.find((d) => d.frontmatter.id === sprintId);
|
|
14524
|
+
} else {
|
|
14525
|
+
sprintDoc = sprintDocs.find((d) => d.frontmatter.status === "active");
|
|
14526
|
+
}
|
|
14527
|
+
if (!sprintDoc) return null;
|
|
14528
|
+
const fm = sprintDoc.frontmatter;
|
|
14529
|
+
const startDate = fm.startDate;
|
|
14530
|
+
const endDate = fm.endDate;
|
|
14531
|
+
const today = /* @__PURE__ */ new Date();
|
|
14532
|
+
const todayStr = today.toISOString().slice(0, 10);
|
|
14533
|
+
let daysElapsed = 0;
|
|
14534
|
+
let daysRemaining = 0;
|
|
14535
|
+
let totalDays = 0;
|
|
14536
|
+
let percentComplete = 0;
|
|
14537
|
+
if (startDate && endDate) {
|
|
14538
|
+
const startMs = new Date(startDate).getTime();
|
|
14539
|
+
const endMs = new Date(endDate).getTime();
|
|
14540
|
+
const todayMs = today.getTime();
|
|
14541
|
+
const msPerDay = 864e5;
|
|
14542
|
+
totalDays = Math.max(1, Math.round((endMs - startMs) / msPerDay));
|
|
14543
|
+
daysElapsed = Math.max(0, Math.round((todayMs - startMs) / msPerDay));
|
|
14544
|
+
daysRemaining = Math.max(0, Math.round((endMs - todayMs) / msPerDay));
|
|
14545
|
+
percentComplete = Math.min(100, Math.round(daysElapsed / totalDays * 100));
|
|
14546
|
+
}
|
|
14547
|
+
const linkedEpicIds = normalizeLinkedEpics(fm.linkedEpics);
|
|
14548
|
+
const epicToTasks = /* @__PURE__ */ new Map();
|
|
14549
|
+
const allTasks = allDocs.filter((d) => d.frontmatter.type === "task");
|
|
14550
|
+
for (const task of allTasks) {
|
|
14551
|
+
const tags = task.frontmatter.tags ?? [];
|
|
14552
|
+
for (const tag of tags) {
|
|
14553
|
+
if (tag.startsWith("epic:")) {
|
|
14554
|
+
const epicId = tag.slice(5);
|
|
14555
|
+
if (!epicToTasks.has(epicId)) epicToTasks.set(epicId, []);
|
|
14556
|
+
epicToTasks.get(epicId).push(task);
|
|
14557
|
+
}
|
|
14558
|
+
}
|
|
14559
|
+
}
|
|
14560
|
+
const linkedEpics = linkedEpicIds.map((epicId) => {
|
|
14561
|
+
const epic = store.get(epicId);
|
|
14562
|
+
const tasks = epicToTasks.get(epicId) ?? [];
|
|
14563
|
+
const tasksDone = tasks.filter((t) => DONE_STATUSES.has(t.frontmatter.status)).length;
|
|
14564
|
+
return {
|
|
14565
|
+
id: epicId,
|
|
14566
|
+
title: epic?.frontmatter.title ?? "(not found)",
|
|
14567
|
+
status: epic?.frontmatter.status ?? "unknown",
|
|
14568
|
+
tasksDone,
|
|
14569
|
+
tasksTotal: tasks.length
|
|
14570
|
+
};
|
|
14571
|
+
});
|
|
14572
|
+
const sprintTag = `sprint:${fm.id}`;
|
|
14573
|
+
const workItemDocs = allDocs.filter(
|
|
14574
|
+
(d) => d.frontmatter.type !== "sprint" && d.frontmatter.type !== "epic" && d.frontmatter.tags?.includes(sprintTag)
|
|
14575
|
+
);
|
|
14576
|
+
const byStatus = {};
|
|
14577
|
+
const byType = {};
|
|
14578
|
+
let doneCount = 0;
|
|
14579
|
+
let inProgressCount = 0;
|
|
14580
|
+
let openCount = 0;
|
|
14581
|
+
let blockedCount = 0;
|
|
14582
|
+
for (const doc of workItemDocs) {
|
|
14583
|
+
const s = doc.frontmatter.status;
|
|
14584
|
+
byStatus[s] = (byStatus[s] ?? 0) + 1;
|
|
14585
|
+
byType[doc.frontmatter.type] = (byType[doc.frontmatter.type] ?? 0) + 1;
|
|
14586
|
+
if (DONE_STATUSES.has(s)) doneCount++;
|
|
14587
|
+
else if (s === "in-progress") inProgressCount++;
|
|
14588
|
+
else if (s === "blocked") blockedCount++;
|
|
14589
|
+
else openCount++;
|
|
14590
|
+
}
|
|
14591
|
+
const workItems = {
|
|
14592
|
+
total: workItemDocs.length,
|
|
14593
|
+
done: doneCount,
|
|
14594
|
+
inProgress: inProgressCount,
|
|
14595
|
+
open: openCount,
|
|
14596
|
+
blocked: blockedCount,
|
|
14597
|
+
completionPct: workItemDocs.length > 0 ? Math.round(doneCount / workItemDocs.length * 100) : 0,
|
|
14598
|
+
byStatus,
|
|
14599
|
+
byType,
|
|
14600
|
+
items: workItemDocs.map((d) => ({
|
|
14601
|
+
id: d.frontmatter.id,
|
|
14602
|
+
title: d.frontmatter.title,
|
|
14603
|
+
type: d.frontmatter.type,
|
|
14604
|
+
status: d.frontmatter.status
|
|
14605
|
+
}))
|
|
14606
|
+
};
|
|
14607
|
+
const meetings = [];
|
|
14608
|
+
if (startDate && endDate) {
|
|
14609
|
+
const meetingDocs = allDocs.filter((d) => d.frontmatter.type === "meeting");
|
|
14610
|
+
for (const m of meetingDocs) {
|
|
14611
|
+
const meetingDate = m.frontmatter.date ?? m.frontmatter.created.slice(0, 10);
|
|
14612
|
+
if (meetingDate >= startDate && meetingDate <= endDate) {
|
|
14613
|
+
meetings.push({
|
|
14614
|
+
id: m.frontmatter.id,
|
|
14615
|
+
title: m.frontmatter.title,
|
|
14616
|
+
date: meetingDate
|
|
14617
|
+
});
|
|
14618
|
+
}
|
|
14619
|
+
}
|
|
14620
|
+
meetings.sort((a, b) => a.date.localeCompare(b.date));
|
|
14621
|
+
}
|
|
14622
|
+
const artifacts = [];
|
|
14623
|
+
if (startDate && endDate) {
|
|
14624
|
+
for (const doc of allDocs) {
|
|
14625
|
+
if (doc.frontmatter.type === "sprint") continue;
|
|
14626
|
+
const created = doc.frontmatter.created.slice(0, 10);
|
|
14627
|
+
const updated = doc.frontmatter.updated.slice(0, 10);
|
|
14628
|
+
if (created >= startDate && created <= endDate) {
|
|
14629
|
+
artifacts.push({
|
|
14630
|
+
id: doc.frontmatter.id,
|
|
14631
|
+
title: doc.frontmatter.title,
|
|
14632
|
+
type: doc.frontmatter.type,
|
|
14633
|
+
action: "created",
|
|
14634
|
+
date: created
|
|
14635
|
+
});
|
|
14636
|
+
} else if (updated >= startDate && updated <= endDate && updated !== created) {
|
|
14637
|
+
artifacts.push({
|
|
14638
|
+
id: doc.frontmatter.id,
|
|
14639
|
+
title: doc.frontmatter.title,
|
|
14640
|
+
type: doc.frontmatter.type,
|
|
14641
|
+
action: "updated",
|
|
14642
|
+
date: updated
|
|
14643
|
+
});
|
|
14644
|
+
}
|
|
14645
|
+
}
|
|
14646
|
+
artifacts.sort((a, b) => b.date.localeCompare(a.date));
|
|
14647
|
+
}
|
|
14648
|
+
const relevantTags = /* @__PURE__ */ new Set([sprintTag, ...linkedEpicIds.map((id) => `epic:${id}`)]);
|
|
14649
|
+
const openActions = allDocs.filter(
|
|
14650
|
+
(d) => d.frontmatter.type === "action" && !DONE_STATUSES.has(d.frontmatter.status) && d.frontmatter.tags?.some((t) => relevantTags.has(t))
|
|
14651
|
+
).map((d) => ({
|
|
14652
|
+
id: d.frontmatter.id,
|
|
14653
|
+
title: d.frontmatter.title,
|
|
14654
|
+
owner: d.frontmatter.owner,
|
|
14655
|
+
dueDate: d.frontmatter.dueDate
|
|
14656
|
+
}));
|
|
14657
|
+
const openQuestions = allDocs.filter(
|
|
14658
|
+
(d) => d.frontmatter.type === "question" && d.frontmatter.status === "open" && d.frontmatter.tags?.some((t) => relevantTags.has(t))
|
|
14659
|
+
).map((d) => ({
|
|
14660
|
+
id: d.frontmatter.id,
|
|
14661
|
+
title: d.frontmatter.title
|
|
14662
|
+
}));
|
|
14663
|
+
const blockers = allDocs.filter(
|
|
14664
|
+
(d) => d.frontmatter.status === "blocked" && d.frontmatter.tags?.includes(sprintTag)
|
|
14665
|
+
).map((d) => ({
|
|
14666
|
+
id: d.frontmatter.id,
|
|
14667
|
+
title: d.frontmatter.title,
|
|
14668
|
+
type: d.frontmatter.type
|
|
14669
|
+
}));
|
|
14670
|
+
const riskBlockers = allDocs.filter(
|
|
14671
|
+
(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)
|
|
14672
|
+
);
|
|
14673
|
+
for (const d of riskBlockers) {
|
|
14674
|
+
blockers.push({
|
|
14675
|
+
id: d.frontmatter.id,
|
|
14676
|
+
title: d.frontmatter.title,
|
|
14677
|
+
type: d.frontmatter.type
|
|
14678
|
+
});
|
|
14679
|
+
}
|
|
14680
|
+
let velocity = null;
|
|
14681
|
+
const currentRate = workItems.completionPct;
|
|
14682
|
+
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 ?? ""));
|
|
14683
|
+
if (completedSprints.length > 0) {
|
|
14684
|
+
const prev = completedSprints[0];
|
|
14685
|
+
const prevTag = `sprint:${prev.frontmatter.id}`;
|
|
14686
|
+
const prevWorkItems = allDocs.filter(
|
|
14687
|
+
(d) => d.frontmatter.type !== "sprint" && d.frontmatter.type !== "epic" && d.frontmatter.tags?.includes(prevTag)
|
|
14688
|
+
);
|
|
14689
|
+
const prevDone = prevWorkItems.filter((d) => DONE_STATUSES.has(d.frontmatter.status)).length;
|
|
14690
|
+
const prevRate = prevWorkItems.length > 0 ? Math.round(prevDone / prevWorkItems.length * 100) : 0;
|
|
14691
|
+
velocity = {
|
|
14692
|
+
currentCompletionRate: currentRate,
|
|
14693
|
+
previousSprintRate: prevRate,
|
|
14694
|
+
previousSprintId: prev.frontmatter.id
|
|
14695
|
+
};
|
|
14696
|
+
} else {
|
|
14697
|
+
velocity = { currentCompletionRate: currentRate };
|
|
14698
|
+
}
|
|
14699
|
+
return {
|
|
14700
|
+
sprint: {
|
|
14701
|
+
id: fm.id,
|
|
14702
|
+
title: fm.title,
|
|
14703
|
+
goal: fm.goal,
|
|
14704
|
+
status: fm.status,
|
|
14705
|
+
startDate,
|
|
14706
|
+
endDate
|
|
14707
|
+
},
|
|
14708
|
+
timeline: { daysElapsed, daysRemaining, totalDays, percentComplete },
|
|
14709
|
+
linkedEpics,
|
|
14710
|
+
workItems,
|
|
14711
|
+
meetings,
|
|
14712
|
+
artifacts,
|
|
14713
|
+
openActions,
|
|
14714
|
+
openQuestions,
|
|
14715
|
+
blockers,
|
|
14716
|
+
velocity
|
|
14717
|
+
};
|
|
14718
|
+
}
|
|
14719
|
+
|
|
14720
|
+
// src/reports/sprint-summary/generator.ts
|
|
14721
|
+
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
14722
|
+
async function generateSprintSummary(data) {
|
|
14723
|
+
const prompt = buildPrompt(data);
|
|
14724
|
+
const result = query({
|
|
14725
|
+
prompt,
|
|
14726
|
+
options: {
|
|
14727
|
+
systemPrompt: SYSTEM_PROMPT,
|
|
14728
|
+
maxTurns: 1,
|
|
14729
|
+
tools: [],
|
|
14730
|
+
allowedTools: []
|
|
14731
|
+
}
|
|
14732
|
+
});
|
|
14733
|
+
for await (const msg of result) {
|
|
14734
|
+
if (msg.type === "assistant") {
|
|
14735
|
+
const text = msg.message.content.find(
|
|
14736
|
+
(b) => b.type === "text"
|
|
14737
|
+
);
|
|
14738
|
+
if (text) return text.text;
|
|
14739
|
+
}
|
|
14740
|
+
}
|
|
14741
|
+
return "Unable to generate sprint summary.";
|
|
14742
|
+
}
|
|
14743
|
+
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:
|
|
14744
|
+
|
|
14745
|
+
## Sprint Health
|
|
14746
|
+
One-line verdict on overall sprint health (healthy / at risk / behind).
|
|
14747
|
+
|
|
14748
|
+
## Goal Progress
|
|
14749
|
+
How close the team is to achieving the sprint goal. Reference the goal text and completion metrics.
|
|
14750
|
+
|
|
14751
|
+
## Key Achievements
|
|
14752
|
+
Notable completions, decisions made, meetings held during the sprint. Use bullet points.
|
|
14753
|
+
|
|
14754
|
+
## Current Risks
|
|
14755
|
+
Blockers, overdue items, unresolved questions, items without owners. Use bullet points. If none, say so.
|
|
14756
|
+
|
|
14757
|
+
## Outcome Projection
|
|
14758
|
+
Given the current pace and time remaining, what's the likely outcome? Will the sprint goal be met?
|
|
14759
|
+
|
|
14760
|
+
Be specific \u2014 reference artifact IDs, dates, and numbers from the data. Keep the tone professional but direct.`;
|
|
14761
|
+
function buildPrompt(data) {
|
|
14762
|
+
const sections = [];
|
|
14763
|
+
sections.push(`# Sprint: ${data.sprint.id} \u2014 ${data.sprint.title}`);
|
|
14764
|
+
sections.push(`Status: ${data.sprint.status}`);
|
|
14765
|
+
if (data.sprint.goal) sections.push(`Goal: ${data.sprint.goal}`);
|
|
14766
|
+
if (data.sprint.startDate) sections.push(`Start: ${data.sprint.startDate}`);
|
|
14767
|
+
if (data.sprint.endDate) sections.push(`End: ${data.sprint.endDate}`);
|
|
14768
|
+
sections.push(`
|
|
14769
|
+
## Timeline`);
|
|
14770
|
+
sections.push(`Days elapsed: ${data.timeline.daysElapsed} / ${data.timeline.totalDays}`);
|
|
14771
|
+
sections.push(`Days remaining: ${data.timeline.daysRemaining}`);
|
|
14772
|
+
sections.push(`Timeline progress: ${data.timeline.percentComplete}%`);
|
|
14773
|
+
sections.push(`
|
|
14774
|
+
## Work Items`);
|
|
14775
|
+
sections.push(`Total: ${data.workItems.total}, Done: ${data.workItems.done}, In Progress: ${data.workItems.inProgress}, Open: ${data.workItems.open}, Blocked: ${data.workItems.blocked}`);
|
|
14776
|
+
sections.push(`Completion: ${data.workItems.completionPct}%`);
|
|
14777
|
+
if (Object.keys(data.workItems.byType).length > 0) {
|
|
14778
|
+
sections.push(`By type: ${Object.entries(data.workItems.byType).map(([t, n]) => `${t}: ${n}`).join(", ")}`);
|
|
14779
|
+
}
|
|
14780
|
+
if (data.linkedEpics.length > 0) {
|
|
14781
|
+
sections.push(`
|
|
14782
|
+
## Linked Epics`);
|
|
14783
|
+
for (const e of data.linkedEpics) {
|
|
14784
|
+
sections.push(`- ${e.id}: ${e.title} [${e.status}] \u2014 ${e.tasksDone}/${e.tasksTotal} tasks done`);
|
|
14785
|
+
}
|
|
14786
|
+
}
|
|
14787
|
+
if (data.meetings.length > 0) {
|
|
14788
|
+
sections.push(`
|
|
14789
|
+
## Meetings During Sprint`);
|
|
14790
|
+
for (const m of data.meetings) {
|
|
14791
|
+
sections.push(`- ${m.date}: ${m.id} \u2014 ${m.title}`);
|
|
14792
|
+
}
|
|
14793
|
+
}
|
|
14794
|
+
if (data.artifacts.length > 0) {
|
|
14795
|
+
sections.push(`
|
|
14796
|
+
## Artifacts Created/Updated During Sprint`);
|
|
14797
|
+
for (const a of data.artifacts.slice(0, 20)) {
|
|
14798
|
+
sections.push(`- ${a.date}: ${a.id} (${a.type}) ${a.action} \u2014 ${a.title}`);
|
|
14799
|
+
}
|
|
14800
|
+
if (data.artifacts.length > 20) {
|
|
14801
|
+
sections.push(`... and ${data.artifacts.length - 20} more`);
|
|
14802
|
+
}
|
|
14803
|
+
}
|
|
14804
|
+
if (data.openActions.length > 0) {
|
|
14805
|
+
sections.push(`
|
|
14806
|
+
## Open Actions`);
|
|
14807
|
+
for (const a of data.openActions) {
|
|
14808
|
+
const owner = a.owner ?? "unowned";
|
|
14809
|
+
const due = a.dueDate ?? "no due date";
|
|
14810
|
+
sections.push(`- ${a.id}: ${a.title} (${owner}, ${due})`);
|
|
14811
|
+
}
|
|
14812
|
+
}
|
|
14813
|
+
if (data.openQuestions.length > 0) {
|
|
14814
|
+
sections.push(`
|
|
14815
|
+
## Open Questions`);
|
|
14816
|
+
for (const q of data.openQuestions) {
|
|
14817
|
+
sections.push(`- ${q.id}: ${q.title}`);
|
|
14818
|
+
}
|
|
14819
|
+
}
|
|
14820
|
+
if (data.blockers.length > 0) {
|
|
14821
|
+
sections.push(`
|
|
14822
|
+
## Blockers`);
|
|
14823
|
+
for (const b of data.blockers) {
|
|
14824
|
+
sections.push(`- ${b.id} (${b.type}): ${b.title}`);
|
|
14825
|
+
}
|
|
14826
|
+
}
|
|
14827
|
+
if (data.velocity) {
|
|
14828
|
+
sections.push(`
|
|
14829
|
+
## Velocity`);
|
|
14830
|
+
sections.push(`Current sprint completion rate: ${data.velocity.currentCompletionRate}%`);
|
|
14831
|
+
if (data.velocity.previousSprintRate !== void 0) {
|
|
14832
|
+
sections.push(`Previous sprint (${data.velocity.previousSprintId}): ${data.velocity.previousSprintRate}%`);
|
|
14833
|
+
}
|
|
14834
|
+
}
|
|
14835
|
+
return sections.join("\n");
|
|
14836
|
+
}
|
|
14837
|
+
|
|
14498
14838
|
// src/plugins/builtin/tools/reports.ts
|
|
14499
14839
|
function createReportTools(store) {
|
|
14500
14840
|
return [
|
|
@@ -14786,6 +15126,25 @@ function createReportTools(store) {
|
|
|
14786
15126
|
},
|
|
14787
15127
|
{ annotations: { readOnlyHint: true } }
|
|
14788
15128
|
),
|
|
15129
|
+
tool2(
|
|
15130
|
+
"generate_sprint_summary",
|
|
15131
|
+
"Generate an AI-powered narrative summary of a sprint's progress, health, achievements, risks, and projected outcome",
|
|
15132
|
+
{
|
|
15133
|
+
sprint: external_exports.string().optional().describe("Sprint ID (e.g. 'SP-001'). Omit for the active sprint.")
|
|
15134
|
+
},
|
|
15135
|
+
async (args) => {
|
|
15136
|
+
const data = collectSprintSummaryData(store, args.sprint);
|
|
15137
|
+
if (!data) {
|
|
15138
|
+
const msg = args.sprint ? `Sprint ${args.sprint} not found.` : "No active sprint found.";
|
|
15139
|
+
return { content: [{ type: "text", text: msg }], isError: true };
|
|
15140
|
+
}
|
|
15141
|
+
const summary = await generateSprintSummary(data);
|
|
15142
|
+
return {
|
|
15143
|
+
content: [{ type: "text", text: summary }]
|
|
15144
|
+
};
|
|
15145
|
+
},
|
|
15146
|
+
{ annotations: { readOnlyHint: true } }
|
|
15147
|
+
),
|
|
14789
15148
|
tool2(
|
|
14790
15149
|
"save_report",
|
|
14791
15150
|
"Save a generated report as a persistent document",
|
|
@@ -15629,26 +15988,6 @@ function createSprintPlanningTools(store) {
|
|
|
15629
15988
|
|
|
15630
15989
|
// src/plugins/builtin/tools/tasks.ts
|
|
15631
15990
|
import { tool as tool8 } from "@anthropic-ai/claude-agent-sdk";
|
|
15632
|
-
|
|
15633
|
-
// src/plugins/builtin/tools/task-utils.ts
|
|
15634
|
-
function normalizeLinkedEpics(value) {
|
|
15635
|
-
if (value === void 0 || value === null) return [];
|
|
15636
|
-
if (typeof value === "string") {
|
|
15637
|
-
try {
|
|
15638
|
-
const parsed = JSON.parse(value);
|
|
15639
|
-
if (Array.isArray(parsed)) return parsed.filter((v) => typeof v === "string");
|
|
15640
|
-
} catch {
|
|
15641
|
-
}
|
|
15642
|
-
return [value];
|
|
15643
|
-
}
|
|
15644
|
-
if (Array.isArray(value)) return value.filter((v) => typeof v === "string");
|
|
15645
|
-
return [];
|
|
15646
|
-
}
|
|
15647
|
-
function generateEpicTags(epics) {
|
|
15648
|
-
return epics.map((id) => `epic:${id}`);
|
|
15649
|
-
}
|
|
15650
|
-
|
|
15651
|
-
// src/plugins/builtin/tools/tasks.ts
|
|
15652
15991
|
var linkedEpicArray = external_exports.preprocess(
|
|
15653
15992
|
(val) => {
|
|
15654
15993
|
if (typeof val === "string") {
|
|
@@ -17243,7 +17582,7 @@ import * as readline from "readline";
|
|
|
17243
17582
|
import chalk2 from "chalk";
|
|
17244
17583
|
import ora from "ora";
|
|
17245
17584
|
import {
|
|
17246
|
-
query as
|
|
17585
|
+
query as query3
|
|
17247
17586
|
} from "@anthropic-ai/claude-agent-sdk";
|
|
17248
17587
|
|
|
17249
17588
|
// src/personas/prompt-builder.ts
|
|
@@ -17383,9 +17722,9 @@ var DocumentStore = class {
|
|
|
17383
17722
|
}
|
|
17384
17723
|
}
|
|
17385
17724
|
}
|
|
17386
|
-
list(
|
|
17725
|
+
list(query8) {
|
|
17387
17726
|
const results = [];
|
|
17388
|
-
const types =
|
|
17727
|
+
const types = query8?.type ? [query8.type] : Object.keys(this.typeDirs);
|
|
17389
17728
|
for (const type of types) {
|
|
17390
17729
|
const dirName = this.typeDirs[type];
|
|
17391
17730
|
if (!dirName) continue;
|
|
@@ -17396,9 +17735,9 @@ var DocumentStore = class {
|
|
|
17396
17735
|
const filePath = path6.join(dir, file2);
|
|
17397
17736
|
const raw = fs6.readFileSync(filePath, "utf-8");
|
|
17398
17737
|
const doc = parseDocument(raw, filePath);
|
|
17399
|
-
if (
|
|
17400
|
-
if (
|
|
17401
|
-
if (
|
|
17738
|
+
if (query8?.status && doc.frontmatter.status !== query8.status) continue;
|
|
17739
|
+
if (query8?.owner && doc.frontmatter.owner !== query8.owner) continue;
|
|
17740
|
+
if (query8?.tag && (!doc.frontmatter.tags || !doc.frontmatter.tags.includes(query8.tag)))
|
|
17402
17741
|
continue;
|
|
17403
17742
|
results.push(doc);
|
|
17404
17743
|
}
|
|
@@ -18364,7 +18703,7 @@ function computeUrgency(dueDateStr, todayStr) {
|
|
|
18364
18703
|
if (diffDays <= 14) return "upcoming";
|
|
18365
18704
|
return "later";
|
|
18366
18705
|
}
|
|
18367
|
-
var
|
|
18706
|
+
var DONE_STATUSES2 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
18368
18707
|
function getUpcomingData(store) {
|
|
18369
18708
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
18370
18709
|
const allDocs = store.list();
|
|
@@ -18373,7 +18712,7 @@ function getUpcomingData(store) {
|
|
|
18373
18712
|
docById.set(doc.frontmatter.id, doc);
|
|
18374
18713
|
}
|
|
18375
18714
|
const actions = allDocs.filter(
|
|
18376
|
-
(d) => d.frontmatter.type === "action" && !
|
|
18715
|
+
(d) => d.frontmatter.type === "action" && !DONE_STATUSES2.has(d.frontmatter.status)
|
|
18377
18716
|
);
|
|
18378
18717
|
const actionsWithDue = actions.filter((d) => d.frontmatter.dueDate);
|
|
18379
18718
|
const sprints = allDocs.filter((d) => d.frontmatter.type === "sprint");
|
|
@@ -18437,7 +18776,7 @@ function getUpcomingData(store) {
|
|
|
18437
18776
|
const sprintEnd = sprint.frontmatter.endDate;
|
|
18438
18777
|
const sprintTaskDocs = getSprintTasks(sprint);
|
|
18439
18778
|
for (const task of sprintTaskDocs) {
|
|
18440
|
-
if (
|
|
18779
|
+
if (DONE_STATUSES2.has(task.frontmatter.status)) continue;
|
|
18441
18780
|
const existing = taskSprintMap.get(task.frontmatter.id);
|
|
18442
18781
|
if (!existing || sprintEnd < existing.sprintEnd) {
|
|
18443
18782
|
taskSprintMap.set(task.frontmatter.id, { task, sprint, sprintEnd });
|
|
@@ -18454,7 +18793,7 @@ function getUpcomingData(store) {
|
|
|
18454
18793
|
urgency: computeUrgency(sprintEnd, today)
|
|
18455
18794
|
})).sort((a, b) => a.sprintEndDate.localeCompare(b.sprintEndDate));
|
|
18456
18795
|
const openItems = allDocs.filter(
|
|
18457
|
-
(d) => ["action", "question", "task"].includes(d.frontmatter.type) && !
|
|
18796
|
+
(d) => ["action", "question", "task"].includes(d.frontmatter.type) && !DONE_STATUSES2.has(d.frontmatter.status)
|
|
18458
18797
|
);
|
|
18459
18798
|
const fourteenDaysAgo = new Date(todayMs - fourteenDaysMs).toISOString().slice(0, 10);
|
|
18460
18799
|
const recentMeetings = allDocs.filter(
|
|
@@ -18552,8 +18891,28 @@ function getUpcomingData(store) {
|
|
|
18552
18891
|
}).filter((item) => item.score > 0).sort((a, b) => b.score - a.score).slice(0, 15);
|
|
18553
18892
|
return { dueSoonActions, dueSoonSprintTasks, trending };
|
|
18554
18893
|
}
|
|
18894
|
+
function getSprintSummaryData(store, sprintId) {
|
|
18895
|
+
return collectSprintSummaryData(store, sprintId);
|
|
18896
|
+
}
|
|
18555
18897
|
|
|
18556
18898
|
// src/web/templates/layout.ts
|
|
18899
|
+
function collapsibleSection(sectionId, title, content, opts) {
|
|
18900
|
+
const tag = opts?.titleTag ?? "div";
|
|
18901
|
+
const cls = opts?.titleClass ?? "section-title";
|
|
18902
|
+
const collapsed = opts?.defaultCollapsed ? " collapsed" : "";
|
|
18903
|
+
return `
|
|
18904
|
+
<div class="collapsible${collapsed}" data-section-id="${escapeHtml(sectionId)}">
|
|
18905
|
+
<${tag} class="${cls} collapsible-header" onclick="toggleSection(this)">
|
|
18906
|
+
<svg class="collapsible-chevron" viewBox="0 0 16 16" width="14" height="14" fill="currentColor">
|
|
18907
|
+
<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"/>
|
|
18908
|
+
</svg>
|
|
18909
|
+
<span>${title}</span>
|
|
18910
|
+
</${tag}>
|
|
18911
|
+
<div class="collapsible-body">
|
|
18912
|
+
${content}
|
|
18913
|
+
</div>
|
|
18914
|
+
</div>`;
|
|
18915
|
+
}
|
|
18557
18916
|
function escapeHtml(str) {
|
|
18558
18917
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
18559
18918
|
}
|
|
@@ -18676,6 +19035,7 @@ function layout(opts, body) {
|
|
|
18676
19035
|
const topItems = [
|
|
18677
19036
|
{ href: "/", label: "Overview" },
|
|
18678
19037
|
{ href: "/upcoming", label: "Upcoming" },
|
|
19038
|
+
{ href: "/sprint-summary", label: "Sprint Summary" },
|
|
18679
19039
|
{ href: "/timeline", label: "Timeline" },
|
|
18680
19040
|
{ href: "/board", label: "Board" },
|
|
18681
19041
|
{ href: "/gar", label: "GAR Report" },
|
|
@@ -18721,6 +19081,32 @@ function layout(opts, body) {
|
|
|
18721
19081
|
${body}
|
|
18722
19082
|
</main>
|
|
18723
19083
|
</div>
|
|
19084
|
+
<script>
|
|
19085
|
+
function toggleSection(header) {
|
|
19086
|
+
var section = header.closest('.collapsible');
|
|
19087
|
+
if (!section) return;
|
|
19088
|
+
section.classList.toggle('collapsed');
|
|
19089
|
+
var id = section.getAttribute('data-section-id');
|
|
19090
|
+
if (id) {
|
|
19091
|
+
try {
|
|
19092
|
+
var state = JSON.parse(localStorage.getItem('marvin-collapsed') || '{}');
|
|
19093
|
+
state[id] = section.classList.contains('collapsed');
|
|
19094
|
+
localStorage.setItem('marvin-collapsed', JSON.stringify(state));
|
|
19095
|
+
} catch(e) {}
|
|
19096
|
+
}
|
|
19097
|
+
}
|
|
19098
|
+
// Restore collapsed state on load
|
|
19099
|
+
(function() {
|
|
19100
|
+
try {
|
|
19101
|
+
var state = JSON.parse(localStorage.getItem('marvin-collapsed') || '{}');
|
|
19102
|
+
document.querySelectorAll('.collapsible[data-section-id]').forEach(function(el) {
|
|
19103
|
+
var id = el.getAttribute('data-section-id');
|
|
19104
|
+
if (state[id] === true) el.classList.add('collapsed');
|
|
19105
|
+
else if (state[id] === false) el.classList.remove('collapsed');
|
|
19106
|
+
});
|
|
19107
|
+
} catch(e) {}
|
|
19108
|
+
})();
|
|
19109
|
+
</script>
|
|
18724
19110
|
<script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
|
|
18725
19111
|
<script>mermaid.initialize({
|
|
18726
19112
|
startOnLoad: true,
|
|
@@ -19544,13 +19930,60 @@ tr:hover td {
|
|
|
19544
19930
|
white-space: nowrap;
|
|
19545
19931
|
}
|
|
19546
19932
|
|
|
19933
|
+
.gantt-grid-line {
|
|
19934
|
+
position: absolute;
|
|
19935
|
+
top: 0;
|
|
19936
|
+
bottom: 0;
|
|
19937
|
+
width: 1px;
|
|
19938
|
+
background: var(--border);
|
|
19939
|
+
opacity: 0.35;
|
|
19940
|
+
}
|
|
19941
|
+
|
|
19942
|
+
.gantt-sprint-line {
|
|
19943
|
+
position: absolute;
|
|
19944
|
+
top: 0;
|
|
19945
|
+
bottom: 0;
|
|
19946
|
+
width: 1px;
|
|
19947
|
+
background: var(--text-dim);
|
|
19948
|
+
opacity: 0.3;
|
|
19949
|
+
}
|
|
19950
|
+
|
|
19547
19951
|
.gantt-today {
|
|
19548
19952
|
position: absolute;
|
|
19549
19953
|
top: 0;
|
|
19550
19954
|
bottom: 0;
|
|
19551
|
-
width:
|
|
19955
|
+
width: 3px;
|
|
19552
19956
|
background: var(--red);
|
|
19553
|
-
opacity: 0.
|
|
19957
|
+
opacity: 0.8;
|
|
19958
|
+
border-radius: 1px;
|
|
19959
|
+
}
|
|
19960
|
+
|
|
19961
|
+
/* Sprint band in timeline */
|
|
19962
|
+
.gantt-sprint-band-row {
|
|
19963
|
+
border-bottom: 1px solid var(--border);
|
|
19964
|
+
margin-bottom: 0.25rem;
|
|
19965
|
+
}
|
|
19966
|
+
|
|
19967
|
+
.gantt-sprint-band {
|
|
19968
|
+
height: 32px;
|
|
19969
|
+
}
|
|
19970
|
+
|
|
19971
|
+
.gantt-sprint-block {
|
|
19972
|
+
position: absolute;
|
|
19973
|
+
top: 2px;
|
|
19974
|
+
bottom: 2px;
|
|
19975
|
+
background: var(--bg-hover);
|
|
19976
|
+
border: 1px solid var(--border);
|
|
19977
|
+
border-radius: 4px;
|
|
19978
|
+
font-size: 0.65rem;
|
|
19979
|
+
color: var(--text-dim);
|
|
19980
|
+
display: flex;
|
|
19981
|
+
align-items: center;
|
|
19982
|
+
justify-content: center;
|
|
19983
|
+
overflow: hidden;
|
|
19984
|
+
white-space: nowrap;
|
|
19985
|
+
text-overflow: ellipsis;
|
|
19986
|
+
padding: 0 0.4rem;
|
|
19554
19987
|
}
|
|
19555
19988
|
|
|
19556
19989
|
/* Pie chart color overrides */
|
|
@@ -19612,6 +20045,146 @@ tr:hover td {
|
|
|
19612
20045
|
}
|
|
19613
20046
|
|
|
19614
20047
|
.text-dim { color: var(--text-dim); }
|
|
20048
|
+
|
|
20049
|
+
/* Sprint Summary */
|
|
20050
|
+
.sprint-goal {
|
|
20051
|
+
background: var(--bg-card);
|
|
20052
|
+
border: 1px solid var(--border);
|
|
20053
|
+
border-radius: var(--radius);
|
|
20054
|
+
padding: 0.75rem 1rem;
|
|
20055
|
+
margin-bottom: 1rem;
|
|
20056
|
+
font-size: 0.9rem;
|
|
20057
|
+
color: var(--text);
|
|
20058
|
+
}
|
|
20059
|
+
|
|
20060
|
+
.sprint-progress-bar {
|
|
20061
|
+
position: relative;
|
|
20062
|
+
height: 24px;
|
|
20063
|
+
background: var(--bg-card);
|
|
20064
|
+
border: 1px solid var(--border);
|
|
20065
|
+
border-radius: 12px;
|
|
20066
|
+
margin-bottom: 1.25rem;
|
|
20067
|
+
overflow: hidden;
|
|
20068
|
+
}
|
|
20069
|
+
|
|
20070
|
+
.sprint-progress-fill {
|
|
20071
|
+
height: 100%;
|
|
20072
|
+
background: linear-gradient(90deg, var(--accent-dim), var(--accent));
|
|
20073
|
+
border-radius: 12px;
|
|
20074
|
+
transition: width 0.3s ease;
|
|
20075
|
+
}
|
|
20076
|
+
|
|
20077
|
+
.sprint-progress-label {
|
|
20078
|
+
position: absolute;
|
|
20079
|
+
top: 50%;
|
|
20080
|
+
left: 50%;
|
|
20081
|
+
transform: translate(-50%, -50%);
|
|
20082
|
+
font-size: 0.7rem;
|
|
20083
|
+
font-weight: 700;
|
|
20084
|
+
color: var(--text);
|
|
20085
|
+
}
|
|
20086
|
+
|
|
20087
|
+
.sprint-ai-section {
|
|
20088
|
+
margin-top: 2rem;
|
|
20089
|
+
background: var(--bg-card);
|
|
20090
|
+
border: 1px solid var(--border);
|
|
20091
|
+
border-radius: var(--radius);
|
|
20092
|
+
padding: 1.5rem;
|
|
20093
|
+
}
|
|
20094
|
+
|
|
20095
|
+
.sprint-ai-section h3 {
|
|
20096
|
+
font-size: 1rem;
|
|
20097
|
+
font-weight: 600;
|
|
20098
|
+
margin-bottom: 0.5rem;
|
|
20099
|
+
}
|
|
20100
|
+
|
|
20101
|
+
.sprint-generate-btn {
|
|
20102
|
+
background: var(--accent);
|
|
20103
|
+
color: #fff;
|
|
20104
|
+
border: none;
|
|
20105
|
+
border-radius: var(--radius);
|
|
20106
|
+
padding: 0.5rem 1.25rem;
|
|
20107
|
+
font-size: 0.85rem;
|
|
20108
|
+
font-weight: 600;
|
|
20109
|
+
cursor: pointer;
|
|
20110
|
+
margin-top: 0.75rem;
|
|
20111
|
+
transition: background 0.15s;
|
|
20112
|
+
}
|
|
20113
|
+
|
|
20114
|
+
.sprint-generate-btn:hover:not(:disabled) {
|
|
20115
|
+
background: var(--accent-dim);
|
|
20116
|
+
}
|
|
20117
|
+
|
|
20118
|
+
.sprint-generate-btn:disabled {
|
|
20119
|
+
opacity: 0.5;
|
|
20120
|
+
cursor: not-allowed;
|
|
20121
|
+
}
|
|
20122
|
+
|
|
20123
|
+
.sprint-loading {
|
|
20124
|
+
display: flex;
|
|
20125
|
+
align-items: center;
|
|
20126
|
+
gap: 0.75rem;
|
|
20127
|
+
padding: 1rem 0;
|
|
20128
|
+
color: var(--text-dim);
|
|
20129
|
+
font-size: 0.85rem;
|
|
20130
|
+
}
|
|
20131
|
+
|
|
20132
|
+
.sprint-spinner {
|
|
20133
|
+
width: 20px;
|
|
20134
|
+
height: 20px;
|
|
20135
|
+
border: 2px solid var(--border);
|
|
20136
|
+
border-top-color: var(--accent);
|
|
20137
|
+
border-radius: 50%;
|
|
20138
|
+
animation: sprint-spin 0.8s linear infinite;
|
|
20139
|
+
}
|
|
20140
|
+
|
|
20141
|
+
@keyframes sprint-spin {
|
|
20142
|
+
to { transform: rotate(360deg); }
|
|
20143
|
+
}
|
|
20144
|
+
|
|
20145
|
+
.sprint-error {
|
|
20146
|
+
color: var(--red);
|
|
20147
|
+
font-size: 0.85rem;
|
|
20148
|
+
padding: 0.5rem 0;
|
|
20149
|
+
}
|
|
20150
|
+
|
|
20151
|
+
.sprint-ai-section .detail-content {
|
|
20152
|
+
margin-top: 1rem;
|
|
20153
|
+
}
|
|
20154
|
+
|
|
20155
|
+
/* Collapsible sections */
|
|
20156
|
+
.collapsible-header {
|
|
20157
|
+
cursor: pointer;
|
|
20158
|
+
display: flex;
|
|
20159
|
+
align-items: center;
|
|
20160
|
+
gap: 0.4rem;
|
|
20161
|
+
user-select: none;
|
|
20162
|
+
}
|
|
20163
|
+
|
|
20164
|
+
.collapsible-header:hover {
|
|
20165
|
+
color: var(--accent);
|
|
20166
|
+
}
|
|
20167
|
+
|
|
20168
|
+
.collapsible-chevron {
|
|
20169
|
+
transition: transform 0.2s ease;
|
|
20170
|
+
flex-shrink: 0;
|
|
20171
|
+
}
|
|
20172
|
+
|
|
20173
|
+
.collapsible.collapsed .collapsible-chevron {
|
|
20174
|
+
transform: rotate(-90deg);
|
|
20175
|
+
}
|
|
20176
|
+
|
|
20177
|
+
.collapsible-body {
|
|
20178
|
+
overflow: hidden;
|
|
20179
|
+
max-height: 5000px;
|
|
20180
|
+
transition: max-height 0.3s ease, opacity 0.2s ease;
|
|
20181
|
+
opacity: 1;
|
|
20182
|
+
}
|
|
20183
|
+
|
|
20184
|
+
.collapsible.collapsed .collapsible-body {
|
|
20185
|
+
max-height: 0;
|
|
20186
|
+
opacity: 0;
|
|
20187
|
+
}
|
|
19615
20188
|
`;
|
|
19616
20189
|
}
|
|
19617
20190
|
|
|
@@ -19664,35 +20237,73 @@ function buildTimelineGantt(data, maxSprints = 6) {
|
|
|
19664
20237
|
);
|
|
19665
20238
|
tick += 7 * DAY;
|
|
19666
20239
|
}
|
|
20240
|
+
const gridLines = [];
|
|
20241
|
+
let gridTick = timelineStart;
|
|
20242
|
+
const gridStartDay = new Date(gridTick).getDay();
|
|
20243
|
+
gridTick += (8 - gridStartDay) % 7 * DAY;
|
|
20244
|
+
while (gridTick <= timelineEnd) {
|
|
20245
|
+
gridLines.push(`<div class="gantt-grid-line" style="left:${pct(gridTick).toFixed(2)}%"></div>`);
|
|
20246
|
+
gridTick += 7 * DAY;
|
|
20247
|
+
}
|
|
20248
|
+
const sprintBoundaries = /* @__PURE__ */ new Set();
|
|
20249
|
+
for (const sprint of visibleSprints) {
|
|
20250
|
+
sprintBoundaries.add(toMs(sprint.startDate));
|
|
20251
|
+
sprintBoundaries.add(toMs(sprint.endDate));
|
|
20252
|
+
}
|
|
20253
|
+
const sprintLines = [...sprintBoundaries].map(
|
|
20254
|
+
(ms) => `<div class="gantt-sprint-line" style="left:${pct(ms).toFixed(2)}%"></div>`
|
|
20255
|
+
);
|
|
19667
20256
|
const now = Date.now();
|
|
19668
20257
|
let todayMarker = "";
|
|
19669
20258
|
if (now >= timelineStart && now <= timelineEnd) {
|
|
19670
20259
|
todayMarker = `<div class="gantt-today" style="left:${pct(now).toFixed(2)}%"></div>`;
|
|
19671
20260
|
}
|
|
19672
|
-
const
|
|
20261
|
+
const sprintBlocks = visibleSprints.map((sprint) => {
|
|
20262
|
+
const sStart = toMs(sprint.startDate);
|
|
20263
|
+
const sEnd = toMs(sprint.endDate);
|
|
20264
|
+
const left = pct(sStart).toFixed(2);
|
|
20265
|
+
const width = (pct(sEnd) - pct(sStart)).toFixed(2);
|
|
20266
|
+
return `<div class="gantt-sprint-block" style="left:${left}%;width:${width}%">${sanitize(sprint.id, 20)}</div>`;
|
|
20267
|
+
}).join("");
|
|
20268
|
+
const sprintBandRow = `<div class="gantt-row gantt-sprint-band-row">
|
|
20269
|
+
<div class="gantt-label gantt-section-label">Sprints</div>
|
|
20270
|
+
<div class="gantt-track gantt-sprint-band">${sprintBlocks}</div>
|
|
20271
|
+
</div>`;
|
|
20272
|
+
const epicSpanMap = /* @__PURE__ */ new Map();
|
|
19673
20273
|
for (const sprint of visibleSprints) {
|
|
19674
20274
|
const sStart = toMs(sprint.startDate);
|
|
19675
20275
|
const sEnd = toMs(sprint.endDate);
|
|
19676
|
-
|
|
19677
|
-
|
|
19678
|
-
|
|
19679
|
-
|
|
19680
|
-
|
|
19681
|
-
|
|
19682
|
-
|
|
19683
|
-
|
|
19684
|
-
|
|
19685
|
-
|
|
19686
|
-
|
|
19687
|
-
|
|
19688
|
-
|
|
19689
|
-
|
|
20276
|
+
for (const eid of sprint.linkedEpics) {
|
|
20277
|
+
if (!epicMap.has(eid)) continue;
|
|
20278
|
+
const existing = epicSpanMap.get(eid);
|
|
20279
|
+
if (existing) {
|
|
20280
|
+
existing.startMs = Math.min(existing.startMs, sStart);
|
|
20281
|
+
existing.endMs = Math.max(existing.endMs, sEnd);
|
|
20282
|
+
} else {
|
|
20283
|
+
epicSpanMap.set(eid, { startMs: sStart, endMs: sEnd });
|
|
20284
|
+
}
|
|
20285
|
+
}
|
|
20286
|
+
}
|
|
20287
|
+
const sortedEpicIds = [...epicSpanMap.keys()].sort((a, b) => {
|
|
20288
|
+
const aSpan = epicSpanMap.get(a);
|
|
20289
|
+
const bSpan = epicSpanMap.get(b);
|
|
20290
|
+
if (aSpan.startMs !== bSpan.startMs) return aSpan.startMs - bSpan.startMs;
|
|
20291
|
+
return a.localeCompare(b);
|
|
20292
|
+
});
|
|
20293
|
+
const epicRows = sortedEpicIds.map((eid) => {
|
|
20294
|
+
const epic = epicMap.get(eid);
|
|
20295
|
+
const { startMs, endMs } = epicSpanMap.get(eid);
|
|
20296
|
+
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";
|
|
20297
|
+
const left = pct(startMs).toFixed(2);
|
|
20298
|
+
const width = (pct(endMs) - pct(startMs)).toFixed(2);
|
|
20299
|
+
const label = sanitize(epic.id + " " + epic.title);
|
|
20300
|
+
return `<div class="gantt-row">
|
|
20301
|
+
<div class="gantt-label">${label}</div>
|
|
19690
20302
|
<div class="gantt-track">
|
|
19691
20303
|
<div class="gantt-bar ${cls}" style="left:${left}%;width:${width}%"></div>
|
|
19692
20304
|
</div>
|
|
19693
|
-
</div
|
|
19694
|
-
|
|
19695
|
-
}
|
|
20305
|
+
</div>`;
|
|
20306
|
+
}).join("\n");
|
|
19696
20307
|
const note = truncated ? `<div class="mermaid-note">${hiddenCount} earlier sprint${hiddenCount > 1 ? "s" : ""} not shown</div>` : "";
|
|
19697
20308
|
return `${note}
|
|
19698
20309
|
<div class="gantt">
|
|
@@ -19701,11 +20312,12 @@ function buildTimelineGantt(data, maxSprints = 6) {
|
|
|
19701
20312
|
<div class="gantt-label"></div>
|
|
19702
20313
|
<div class="gantt-track gantt-dates">${markers.join("")}</div>
|
|
19703
20314
|
</div>
|
|
19704
|
-
${
|
|
20315
|
+
${sprintBandRow}
|
|
20316
|
+
${epicRows}
|
|
19705
20317
|
</div>
|
|
19706
20318
|
<div class="gantt-overlay">
|
|
19707
20319
|
<div class="gantt-label"></div>
|
|
19708
|
-
<div class="gantt-track">${todayMarker}</div>
|
|
20320
|
+
<div class="gantt-track">${gridLines.join("")}${sprintLines.join("")}${todayMarker}</div>
|
|
19709
20321
|
</div>
|
|
19710
20322
|
</div>`;
|
|
19711
20323
|
}
|
|
@@ -19985,11 +20597,12 @@ function overviewPage(data, diagrams, navGroups) {
|
|
|
19985
20597
|
|
|
19986
20598
|
<div class="section-title"><a href="/timeline">Project Timeline →</a></div>
|
|
19987
20599
|
|
|
19988
|
-
|
|
19989
|
-
${buildArtifactFlowchart(diagrams)}
|
|
20600
|
+
${collapsibleSection("overview-relationships", "Artifact Relationships", buildArtifactFlowchart(diagrams))}
|
|
19990
20601
|
|
|
19991
|
-
|
|
19992
|
-
|
|
20602
|
+
${collapsibleSection(
|
|
20603
|
+
"overview-recent",
|
|
20604
|
+
"Recent Activity",
|
|
20605
|
+
data.recent.length > 0 ? `
|
|
19993
20606
|
<div class="table-wrap">
|
|
19994
20607
|
<table>
|
|
19995
20608
|
<thead>
|
|
@@ -20005,7 +20618,8 @@ function overviewPage(data, diagrams, navGroups) {
|
|
|
20005
20618
|
${rows}
|
|
20006
20619
|
</tbody>
|
|
20007
20620
|
</table>
|
|
20008
|
-
</div>` : `<div class="empty"><p>No documents yet.</p></div>`
|
|
20621
|
+
</div>` : `<div class="empty"><p>No documents yet.</p></div>`
|
|
20622
|
+
)}
|
|
20009
20623
|
`;
|
|
20010
20624
|
}
|
|
20011
20625
|
|
|
@@ -20150,23 +20764,24 @@ function garPage(report) {
|
|
|
20150
20764
|
<div class="label">Overall: ${escapeHtml(report.overall)}</div>
|
|
20151
20765
|
</div>
|
|
20152
20766
|
|
|
20153
|
-
|
|
20154
|
-
${areaCards}
|
|
20155
|
-
</div>
|
|
20767
|
+
${collapsibleSection("gar-areas", "Areas", `<div class="gar-areas">${areaCards}</div>`)}
|
|
20156
20768
|
|
|
20157
|
-
|
|
20158
|
-
|
|
20159
|
-
|
|
20160
|
-
|
|
20161
|
-
|
|
20162
|
-
|
|
20769
|
+
${collapsibleSection(
|
|
20770
|
+
"gar-status-dist",
|
|
20771
|
+
"Status Distribution",
|
|
20772
|
+
buildStatusPie("Action Status", {
|
|
20773
|
+
Open: report.metrics.scope.open,
|
|
20774
|
+
Done: report.metrics.scope.done,
|
|
20775
|
+
"In Progress": Math.max(0, report.metrics.scope.total - report.metrics.scope.open - report.metrics.scope.done)
|
|
20776
|
+
})
|
|
20777
|
+
)}
|
|
20163
20778
|
`;
|
|
20164
20779
|
}
|
|
20165
20780
|
|
|
20166
20781
|
// src/web/templates/pages/health.ts
|
|
20167
20782
|
function healthPage(report, metrics) {
|
|
20168
20783
|
const dotClass = `dot-${report.overall}`;
|
|
20169
|
-
function renderSection(title, categories) {
|
|
20784
|
+
function renderSection(sectionId, title, categories) {
|
|
20170
20785
|
const cards = categories.map(
|
|
20171
20786
|
(cat) => `
|
|
20172
20787
|
<div class="gar-area">
|
|
@@ -20178,10 +20793,9 @@ function healthPage(report, metrics) {
|
|
|
20178
20793
|
${cat.items.length > 0 ? `<ul>${cat.items.map((item) => `<li><span class="ref-id">${escapeHtml(item.id)}</span>${escapeHtml(item.detail)}</li>`).join("")}</ul>` : ""}
|
|
20179
20794
|
</div>`
|
|
20180
20795
|
).join("\n");
|
|
20181
|
-
return
|
|
20182
|
-
|
|
20183
|
-
|
|
20184
|
-
`;
|
|
20796
|
+
return collapsibleSection(sectionId, title, `<div class="gar-areas">${cards}</div>`, {
|
|
20797
|
+
titleClass: "health-section-title"
|
|
20798
|
+
});
|
|
20185
20799
|
}
|
|
20186
20800
|
return `
|
|
20187
20801
|
<div class="page-header">
|
|
@@ -20194,35 +20808,43 @@ function healthPage(report, metrics) {
|
|
|
20194
20808
|
<div class="label">Overall: ${escapeHtml(report.overall)}</div>
|
|
20195
20809
|
</div>
|
|
20196
20810
|
|
|
20197
|
-
${renderSection("Completeness", report.completeness)}
|
|
20198
|
-
|
|
20199
|
-
|
|
20200
|
-
|
|
20201
|
-
|
|
20202
|
-
|
|
20203
|
-
|
|
20204
|
-
|
|
20205
|
-
|
|
20206
|
-
|
|
20207
|
-
|
|
20208
|
-
|
|
20209
|
-
|
|
20210
|
-
|
|
20211
|
-
|
|
20212
|
-
|
|
20811
|
+
${renderSection("health-completeness", "Completeness", report.completeness)}
|
|
20812
|
+
|
|
20813
|
+
${collapsibleSection(
|
|
20814
|
+
"health-completeness-overview",
|
|
20815
|
+
"Completeness Overview",
|
|
20816
|
+
buildHealthGauge(
|
|
20817
|
+
metrics ? Object.entries(metrics.completeness).map(([name, cat]) => ({
|
|
20818
|
+
name: name.replace(/\b\w/g, (c) => c.toUpperCase()),
|
|
20819
|
+
complete: cat.complete,
|
|
20820
|
+
total: cat.total
|
|
20821
|
+
})) : report.completeness.map((c) => {
|
|
20822
|
+
const match = c.summary.match(/(\d+)\s*\/\s*(\d+)/);
|
|
20823
|
+
return {
|
|
20824
|
+
name: c.name,
|
|
20825
|
+
complete: match ? parseInt(match[1], 10) : 0,
|
|
20826
|
+
total: match ? parseInt(match[2], 10) : 0
|
|
20827
|
+
};
|
|
20828
|
+
})
|
|
20829
|
+
),
|
|
20830
|
+
{ titleClass: "health-section-title" }
|
|
20213
20831
|
)}
|
|
20214
20832
|
|
|
20215
|
-
${renderSection("Process", report.process)}
|
|
20216
|
-
|
|
20217
|
-
|
|
20218
|
-
|
|
20219
|
-
|
|
20220
|
-
"
|
|
20221
|
-
|
|
20222
|
-
|
|
20223
|
-
|
|
20224
|
-
|
|
20225
|
-
|
|
20833
|
+
${renderSection("health-process", "Process", report.process)}
|
|
20834
|
+
|
|
20835
|
+
${collapsibleSection(
|
|
20836
|
+
"health-process-summary",
|
|
20837
|
+
"Process Summary",
|
|
20838
|
+
metrics ? buildStatusPie("Process Health", {
|
|
20839
|
+
Stale: metrics.process.stale.length,
|
|
20840
|
+
"Aging Actions": metrics.process.agingActions.length,
|
|
20841
|
+
Healthy: Math.max(
|
|
20842
|
+
0,
|
|
20843
|
+
(metrics.completeness ? Object.values(metrics.completeness).reduce((sum, c) => sum + c.total, 0) : 0) - metrics.process.stale.length - metrics.process.agingActions.length
|
|
20844
|
+
)
|
|
20845
|
+
}) : "",
|
|
20846
|
+
{ titleClass: "health-section-title" }
|
|
20847
|
+
)}
|
|
20226
20848
|
`;
|
|
20227
20849
|
}
|
|
20228
20850
|
|
|
@@ -20280,7 +20902,7 @@ function timelinePage(diagrams) {
|
|
|
20280
20902
|
return `
|
|
20281
20903
|
<div class="page-header">
|
|
20282
20904
|
<h2>Project Timeline</h2>
|
|
20283
|
-
<div class="subtitle">
|
|
20905
|
+
<div class="subtitle">Epic timeline across sprints</div>
|
|
20284
20906
|
</div>
|
|
20285
20907
|
|
|
20286
20908
|
${buildTimelineGantt(diagrams)}
|
|
@@ -20308,9 +20930,10 @@ function upcomingPage(data) {
|
|
|
20308
20930
|
const hasActions = data.dueSoonActions.length > 0;
|
|
20309
20931
|
const hasSprintTasks = data.dueSoonSprintTasks.length > 0;
|
|
20310
20932
|
const hasTrending = data.trending.length > 0;
|
|
20311
|
-
const actionsTable = hasActions ?
|
|
20312
|
-
|
|
20313
|
-
|
|
20933
|
+
const actionsTable = hasActions ? collapsibleSection(
|
|
20934
|
+
"upcoming-actions",
|
|
20935
|
+
"Due Soon \u2014 Actions",
|
|
20936
|
+
`<div class="table-wrap">
|
|
20314
20937
|
<table>
|
|
20315
20938
|
<thead>
|
|
20316
20939
|
<tr>
|
|
@@ -20325,7 +20948,7 @@ function upcomingPage(data) {
|
|
|
20325
20948
|
</thead>
|
|
20326
20949
|
<tbody>
|
|
20327
20950
|
${data.dueSoonActions.map(
|
|
20328
|
-
|
|
20951
|
+
(a) => `
|
|
20329
20952
|
<tr class="${urgencyRowClass(a.urgency)}">
|
|
20330
20953
|
<td><a href="/docs/action/${escapeHtml(a.id)}">${escapeHtml(a.id)}</a></td>
|
|
20331
20954
|
<td>${escapeHtml(a.title)}</td>
|
|
@@ -20335,13 +20958,16 @@ function upcomingPage(data) {
|
|
|
20335
20958
|
<td>${urgencyBadge(a.urgency)}</td>
|
|
20336
20959
|
<td>${a.relatedTaskCount > 0 ? a.relatedTaskCount : "\u2014"}</td>
|
|
20337
20960
|
</tr>`
|
|
20338
|
-
|
|
20961
|
+
).join("")}
|
|
20339
20962
|
</tbody>
|
|
20340
20963
|
</table>
|
|
20341
|
-
</div
|
|
20342
|
-
|
|
20343
|
-
|
|
20344
|
-
|
|
20964
|
+
</div>`,
|
|
20965
|
+
{ titleTag: "h3" }
|
|
20966
|
+
) : "";
|
|
20967
|
+
const sprintTasksTable = hasSprintTasks ? collapsibleSection(
|
|
20968
|
+
"upcoming-sprint-tasks",
|
|
20969
|
+
"Due Soon \u2014 Sprint Tasks",
|
|
20970
|
+
`<div class="table-wrap">
|
|
20345
20971
|
<table>
|
|
20346
20972
|
<thead>
|
|
20347
20973
|
<tr>
|
|
@@ -20355,7 +20981,7 @@ function upcomingPage(data) {
|
|
|
20355
20981
|
</thead>
|
|
20356
20982
|
<tbody>
|
|
20357
20983
|
${data.dueSoonSprintTasks.map(
|
|
20358
|
-
|
|
20984
|
+
(t) => `
|
|
20359
20985
|
<tr class="${urgencyRowClass(t.urgency)}">
|
|
20360
20986
|
<td><a href="/docs/task/${escapeHtml(t.id)}">${escapeHtml(t.id)}</a></td>
|
|
20361
20987
|
<td>${escapeHtml(t.title)}</td>
|
|
@@ -20364,13 +20990,16 @@ function upcomingPage(data) {
|
|
|
20364
20990
|
<td>${formatDate(t.sprintEndDate)}</td>
|
|
20365
20991
|
<td>${urgencyBadge(t.urgency)}</td>
|
|
20366
20992
|
</tr>`
|
|
20367
|
-
|
|
20993
|
+
).join("")}
|
|
20368
20994
|
</tbody>
|
|
20369
20995
|
</table>
|
|
20370
|
-
</div
|
|
20371
|
-
|
|
20372
|
-
|
|
20373
|
-
|
|
20996
|
+
</div>`,
|
|
20997
|
+
{ titleTag: "h3" }
|
|
20998
|
+
) : "";
|
|
20999
|
+
const trendingTable = hasTrending ? collapsibleSection(
|
|
21000
|
+
"upcoming-trending",
|
|
21001
|
+
"Trending",
|
|
21002
|
+
`<div class="table-wrap">
|
|
20374
21003
|
<table>
|
|
20375
21004
|
<thead>
|
|
20376
21005
|
<tr>
|
|
@@ -20385,7 +21014,7 @@ function upcomingPage(data) {
|
|
|
20385
21014
|
</thead>
|
|
20386
21015
|
<tbody>
|
|
20387
21016
|
${data.trending.map(
|
|
20388
|
-
|
|
21017
|
+
(t, i) => `
|
|
20389
21018
|
<tr>
|
|
20390
21019
|
<td><span class="trending-rank">${i + 1}</span></td>
|
|
20391
21020
|
<td><a href="/docs/${escapeHtml(t.type)}/${escapeHtml(t.id)}">${escapeHtml(t.id)}</a></td>
|
|
@@ -20395,10 +21024,12 @@ function upcomingPage(data) {
|
|
|
20395
21024
|
<td><span class="trending-score">${t.score}</span></td>
|
|
20396
21025
|
<td>${t.signals.map((s) => `<span class="signal-tag">${escapeHtml(s.factor)} +${s.points}</span>`).join(" ")}</td>
|
|
20397
21026
|
</tr>`
|
|
20398
|
-
|
|
21027
|
+
).join("")}
|
|
20399
21028
|
</tbody>
|
|
20400
21029
|
</table>
|
|
20401
|
-
</div
|
|
21030
|
+
</div>`,
|
|
21031
|
+
{ titleTag: "h3" }
|
|
21032
|
+
) : "";
|
|
20402
21033
|
const emptyState = !hasActions && !hasSprintTasks && !hasTrending ? '<div class="empty"><p>No upcoming items or trending activity found.</p></div>' : "";
|
|
20403
21034
|
return `
|
|
20404
21035
|
<div class="page-header">
|
|
@@ -20412,7 +21043,199 @@ function upcomingPage(data) {
|
|
|
20412
21043
|
`;
|
|
20413
21044
|
}
|
|
20414
21045
|
|
|
21046
|
+
// src/web/templates/pages/sprint-summary.ts
|
|
21047
|
+
function progressBar(pct) {
|
|
21048
|
+
return `<div class="sprint-progress-bar">
|
|
21049
|
+
<div class="sprint-progress-fill" style="width: ${pct}%"></div>
|
|
21050
|
+
<span class="sprint-progress-label">${pct}%</span>
|
|
21051
|
+
</div>`;
|
|
21052
|
+
}
|
|
21053
|
+
function sprintSummaryPage(data, cached2) {
|
|
21054
|
+
if (!data) {
|
|
21055
|
+
return `
|
|
21056
|
+
<div class="page-header">
|
|
21057
|
+
<h2>Sprint Summary</h2>
|
|
21058
|
+
<div class="subtitle">AI-powered sprint narrative</div>
|
|
21059
|
+
</div>
|
|
21060
|
+
<div class="empty">
|
|
21061
|
+
<h3>No Active Sprint</h3>
|
|
21062
|
+
<p>No active sprint found. Create a sprint and set its status to "active" to see the summary.</p>
|
|
21063
|
+
</div>`;
|
|
21064
|
+
}
|
|
21065
|
+
const statsCards = `
|
|
21066
|
+
<div class="cards">
|
|
21067
|
+
<div class="card">
|
|
21068
|
+
<div class="card-label">Completion</div>
|
|
21069
|
+
<div class="card-value">${data.workItems.completionPct}%</div>
|
|
21070
|
+
<div class="card-sub">${data.workItems.done} / ${data.workItems.total} items done</div>
|
|
21071
|
+
</div>
|
|
21072
|
+
<div class="card">
|
|
21073
|
+
<div class="card-label">Days Remaining</div>
|
|
21074
|
+
<div class="card-value">${data.timeline.daysRemaining}</div>
|
|
21075
|
+
<div class="card-sub">${data.timeline.daysElapsed} of ${data.timeline.totalDays} elapsed</div>
|
|
21076
|
+
</div>
|
|
21077
|
+
<div class="card">
|
|
21078
|
+
<div class="card-label">Epics</div>
|
|
21079
|
+
<div class="card-value">${data.linkedEpics.length}</div>
|
|
21080
|
+
<div class="card-sub">linked to sprint</div>
|
|
21081
|
+
</div>
|
|
21082
|
+
<div class="card">
|
|
21083
|
+
<div class="card-label">Blockers</div>
|
|
21084
|
+
<div class="card-value${data.blockers.length > 0 ? " priority-high" : ""}">${data.blockers.length}</div>
|
|
21085
|
+
<div class="card-sub">${data.openActions.length} open actions</div>
|
|
21086
|
+
</div>
|
|
21087
|
+
</div>`;
|
|
21088
|
+
const epicsTable = data.linkedEpics.length > 0 ? collapsibleSection(
|
|
21089
|
+
"ss-epics",
|
|
21090
|
+
"Linked Epics",
|
|
21091
|
+
`<div class="table-wrap">
|
|
21092
|
+
<table>
|
|
21093
|
+
<thead>
|
|
21094
|
+
<tr><th>ID</th><th>Title</th><th>Status</th><th>Tasks</th></tr>
|
|
21095
|
+
</thead>
|
|
21096
|
+
<tbody>
|
|
21097
|
+
${data.linkedEpics.map((e) => `
|
|
21098
|
+
<tr>
|
|
21099
|
+
<td><a href="/docs/epic/${escapeHtml(e.id)}">${escapeHtml(e.id)}</a></td>
|
|
21100
|
+
<td>${escapeHtml(e.title)}</td>
|
|
21101
|
+
<td>${statusBadge(e.status)}</td>
|
|
21102
|
+
<td>${e.tasksDone} / ${e.tasksTotal}</td>
|
|
21103
|
+
</tr>`).join("")}
|
|
21104
|
+
</tbody>
|
|
21105
|
+
</table>
|
|
21106
|
+
</div>`,
|
|
21107
|
+
{ titleTag: "h3" }
|
|
21108
|
+
) : "";
|
|
21109
|
+
const workItemsSection = data.workItems.total > 0 ? collapsibleSection(
|
|
21110
|
+
"ss-work-items",
|
|
21111
|
+
"Work Items",
|
|
21112
|
+
`<div class="table-wrap">
|
|
21113
|
+
<table>
|
|
21114
|
+
<thead>
|
|
21115
|
+
<tr><th>ID</th><th>Title</th><th>Type</th><th>Status</th></tr>
|
|
21116
|
+
</thead>
|
|
21117
|
+
<tbody>
|
|
21118
|
+
${data.workItems.items.map((w) => `
|
|
21119
|
+
<tr>
|
|
21120
|
+
<td><a href="/docs/${escapeHtml(w.type)}/${escapeHtml(w.id)}">${escapeHtml(w.id)}</a></td>
|
|
21121
|
+
<td>${escapeHtml(w.title)}</td>
|
|
21122
|
+
<td>${escapeHtml(typeLabel(w.type))}</td>
|
|
21123
|
+
<td>${statusBadge(w.status)}</td>
|
|
21124
|
+
</tr>`).join("")}
|
|
21125
|
+
</tbody>
|
|
21126
|
+
</table>
|
|
21127
|
+
</div>`,
|
|
21128
|
+
{ titleTag: "h3", defaultCollapsed: true }
|
|
21129
|
+
) : "";
|
|
21130
|
+
const activitySection = data.artifacts.length > 0 ? collapsibleSection(
|
|
21131
|
+
"ss-activity",
|
|
21132
|
+
"Recent Activity",
|
|
21133
|
+
`<div class="table-wrap">
|
|
21134
|
+
<table>
|
|
21135
|
+
<thead>
|
|
21136
|
+
<tr><th>Date</th><th>ID</th><th>Title</th><th>Type</th><th>Action</th></tr>
|
|
21137
|
+
</thead>
|
|
21138
|
+
<tbody>
|
|
21139
|
+
${data.artifacts.slice(0, 15).map((a) => `
|
|
21140
|
+
<tr>
|
|
21141
|
+
<td>${formatDate(a.date)}</td>
|
|
21142
|
+
<td><a href="/docs/${escapeHtml(a.type)}/${escapeHtml(a.id)}">${escapeHtml(a.id)}</a></td>
|
|
21143
|
+
<td>${escapeHtml(a.title)}</td>
|
|
21144
|
+
<td>${escapeHtml(typeLabel(a.type))}</td>
|
|
21145
|
+
<td>${escapeHtml(a.action)}</td>
|
|
21146
|
+
</tr>`).join("")}
|
|
21147
|
+
</tbody>
|
|
21148
|
+
</table>
|
|
21149
|
+
</div>`,
|
|
21150
|
+
{ titleTag: "h3", defaultCollapsed: true }
|
|
21151
|
+
) : "";
|
|
21152
|
+
const meetingsSection = data.meetings.length > 0 ? collapsibleSection(
|
|
21153
|
+
"ss-meetings",
|
|
21154
|
+
`Meetings (${data.meetings.length})`,
|
|
21155
|
+
`<div class="table-wrap">
|
|
21156
|
+
<table>
|
|
21157
|
+
<thead>
|
|
21158
|
+
<tr><th>Date</th><th>ID</th><th>Title</th></tr>
|
|
21159
|
+
</thead>
|
|
21160
|
+
<tbody>
|
|
21161
|
+
${data.meetings.map((m) => `
|
|
21162
|
+
<tr>
|
|
21163
|
+
<td>${formatDate(m.date)}</td>
|
|
21164
|
+
<td><a href="/docs/meeting/${escapeHtml(m.id)}">${escapeHtml(m.id)}</a></td>
|
|
21165
|
+
<td>${escapeHtml(m.title)}</td>
|
|
21166
|
+
</tr>`).join("")}
|
|
21167
|
+
</tbody>
|
|
21168
|
+
</table>
|
|
21169
|
+
</div>`,
|
|
21170
|
+
{ titleTag: "h3", defaultCollapsed: true }
|
|
21171
|
+
) : "";
|
|
21172
|
+
const goalHtml = data.sprint.goal ? `<div class="sprint-goal"><strong>Goal:</strong> ${escapeHtml(data.sprint.goal)}</div>` : "";
|
|
21173
|
+
const dateRange = data.sprint.startDate && data.sprint.endDate ? `<span class="text-dim">${formatDate(data.sprint.startDate)} \u2014 ${formatDate(data.sprint.endDate)}</span>` : "";
|
|
21174
|
+
return `
|
|
21175
|
+
<div class="page-header">
|
|
21176
|
+
<h2>${escapeHtml(data.sprint.id)} \u2014 ${escapeHtml(data.sprint.title)} ${statusBadge(data.sprint.status)}</h2>
|
|
21177
|
+
<div class="subtitle">Sprint Summary ${dateRange}</div>
|
|
21178
|
+
</div>
|
|
21179
|
+
${goalHtml}
|
|
21180
|
+
${progressBar(data.timeline.percentComplete)}
|
|
21181
|
+
${statsCards}
|
|
21182
|
+
${epicsTable}
|
|
21183
|
+
${workItemsSection}
|
|
21184
|
+
${activitySection}
|
|
21185
|
+
${meetingsSection}
|
|
21186
|
+
|
|
21187
|
+
<div class="sprint-ai-section">
|
|
21188
|
+
<h3>AI Summary</h3>
|
|
21189
|
+
${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>`}
|
|
21190
|
+
<button class="sprint-generate-btn" onclick="generateSummary()" id="generate-btn">${cached2 ? "Regenerate" : "Generate AI Summary"}</button>
|
|
21191
|
+
<div id="summary-loading" class="sprint-loading" style="display:none">
|
|
21192
|
+
<div class="sprint-spinner"></div>
|
|
21193
|
+
<span>Generating summary...</span>
|
|
21194
|
+
</div>
|
|
21195
|
+
<div id="summary-error" class="sprint-error" style="display:none"></div>
|
|
21196
|
+
<div id="summary-content" class="detail-content"${cached2 ? "" : ' style="display:none"'}>${cached2 ? cached2.html : ""}</div>
|
|
21197
|
+
</div>
|
|
21198
|
+
|
|
21199
|
+
<script>
|
|
21200
|
+
async function generateSummary() {
|
|
21201
|
+
var btn = document.getElementById('generate-btn');
|
|
21202
|
+
var loading = document.getElementById('summary-loading');
|
|
21203
|
+
var errorEl = document.getElementById('summary-error');
|
|
21204
|
+
var content = document.getElementById('summary-content');
|
|
21205
|
+
|
|
21206
|
+
btn.disabled = true;
|
|
21207
|
+
btn.style.display = 'none';
|
|
21208
|
+
loading.style.display = 'flex';
|
|
21209
|
+
errorEl.style.display = 'none';
|
|
21210
|
+
content.style.display = 'none';
|
|
21211
|
+
|
|
21212
|
+
try {
|
|
21213
|
+
var res = await fetch('/api/sprint-summary', {
|
|
21214
|
+
method: 'POST',
|
|
21215
|
+
headers: { 'Content-Type': 'application/json' },
|
|
21216
|
+
body: JSON.stringify({ sprintId: '${escapeHtml(data.sprint.id)}' })
|
|
21217
|
+
});
|
|
21218
|
+
var json = await res.json();
|
|
21219
|
+
if (!res.ok) throw new Error(json.error || 'Failed to generate summary');
|
|
21220
|
+
loading.style.display = 'none';
|
|
21221
|
+
content.innerHTML = json.html;
|
|
21222
|
+
content.style.display = 'block';
|
|
21223
|
+
btn.textContent = 'Regenerate';
|
|
21224
|
+
btn.style.display = '';
|
|
21225
|
+
btn.disabled = false;
|
|
21226
|
+
} catch (e) {
|
|
21227
|
+
loading.style.display = 'none';
|
|
21228
|
+
errorEl.textContent = e.message;
|
|
21229
|
+
errorEl.style.display = 'block';
|
|
21230
|
+
btn.style.display = '';
|
|
21231
|
+
btn.disabled = false;
|
|
21232
|
+
}
|
|
21233
|
+
}
|
|
21234
|
+
</script>`;
|
|
21235
|
+
}
|
|
21236
|
+
|
|
20415
21237
|
// src/web/router.ts
|
|
21238
|
+
var sprintSummaryCache = /* @__PURE__ */ new Map();
|
|
20416
21239
|
function handleRequest(req, res, store, projectName, navGroups) {
|
|
20417
21240
|
const parsed = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
20418
21241
|
const pathname = parsed.pathname;
|
|
@@ -20458,6 +21281,42 @@ function handleRequest(req, res, store, projectName, navGroups) {
|
|
|
20458
21281
|
respond(res, layout({ title: "Upcoming", activePath: "/upcoming", projectName, navGroups }, body));
|
|
20459
21282
|
return;
|
|
20460
21283
|
}
|
|
21284
|
+
if (pathname === "/sprint-summary" && req.method === "GET") {
|
|
21285
|
+
const sprintId = parsed.searchParams.get("sprint") ?? void 0;
|
|
21286
|
+
const data = getSprintSummaryData(store, sprintId);
|
|
21287
|
+
const cached2 = data ? sprintSummaryCache.get(data.sprint.id) : void 0;
|
|
21288
|
+
const body = sprintSummaryPage(data, cached2 ? { html: cached2.html, generatedAt: cached2.generatedAt } : void 0);
|
|
21289
|
+
respond(res, layout({ title: "Sprint Summary", activePath: "/sprint-summary", projectName, navGroups }, body));
|
|
21290
|
+
return;
|
|
21291
|
+
}
|
|
21292
|
+
if (pathname === "/api/sprint-summary" && req.method === "POST") {
|
|
21293
|
+
let bodyStr = "";
|
|
21294
|
+
req.on("data", (chunk) => {
|
|
21295
|
+
bodyStr += chunk;
|
|
21296
|
+
});
|
|
21297
|
+
req.on("end", async () => {
|
|
21298
|
+
try {
|
|
21299
|
+
const { sprintId } = JSON.parse(bodyStr || "{}");
|
|
21300
|
+
const data = getSprintSummaryData(store, sprintId);
|
|
21301
|
+
if (!data) {
|
|
21302
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
21303
|
+
res.end(JSON.stringify({ error: "Sprint not found" }));
|
|
21304
|
+
return;
|
|
21305
|
+
}
|
|
21306
|
+
const summary = await generateSprintSummary(data);
|
|
21307
|
+
const html = renderMarkdown(summary);
|
|
21308
|
+
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
21309
|
+
sprintSummaryCache.set(data.sprint.id, { html, generatedAt });
|
|
21310
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
21311
|
+
res.end(JSON.stringify({ summary, html, generatedAt }));
|
|
21312
|
+
} catch (err) {
|
|
21313
|
+
console.error("[marvin web] Sprint summary generation error:", err);
|
|
21314
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
21315
|
+
res.end(JSON.stringify({ error: "Failed to generate summary" }));
|
|
21316
|
+
}
|
|
21317
|
+
});
|
|
21318
|
+
return;
|
|
21319
|
+
}
|
|
20461
21320
|
const boardMatch = pathname.match(/^\/board(?:\/([^/]+))?$/);
|
|
20462
21321
|
if (boardMatch) {
|
|
20463
21322
|
const type = boardMatch[1];
|
|
@@ -21115,8 +21974,8 @@ function gatherContext(store, focusFeature, includeDecisions = true, includeQues
|
|
|
21115
21974
|
title: e.frontmatter.title,
|
|
21116
21975
|
status: e.frontmatter.status,
|
|
21117
21976
|
linkedFeature: normalizeLinkedFeatures(e.frontmatter.linkedFeature),
|
|
21118
|
-
targetDate: e.frontmatter.targetDate
|
|
21119
|
-
estimatedEffort: e.frontmatter.estimatedEffort
|
|
21977
|
+
targetDate: typeof e.frontmatter.targetDate === "string" ? e.frontmatter.targetDate : null,
|
|
21978
|
+
estimatedEffort: typeof e.frontmatter.estimatedEffort === "string" ? e.frontmatter.estimatedEffort : null,
|
|
21120
21979
|
content: e.content,
|
|
21121
21980
|
linkedTaskCount: tasks.filter(
|
|
21122
21981
|
(t) => normalizeLinkedEpics(t.frontmatter.linkedEpic).includes(e.frontmatter.id)
|
|
@@ -21127,10 +21986,10 @@ function gatherContext(store, focusFeature, includeDecisions = true, includeQues
|
|
|
21127
21986
|
title: t.frontmatter.title,
|
|
21128
21987
|
status: t.frontmatter.status,
|
|
21129
21988
|
linkedEpic: normalizeLinkedEpics(t.frontmatter.linkedEpic),
|
|
21130
|
-
acceptanceCriteria: t.frontmatter.acceptanceCriteria
|
|
21131
|
-
technicalNotes: t.frontmatter.technicalNotes
|
|
21132
|
-
complexity: t.frontmatter.complexity
|
|
21133
|
-
estimatedPoints: t.frontmatter.estimatedPoints
|
|
21989
|
+
acceptanceCriteria: typeof t.frontmatter.acceptanceCriteria === "string" ? t.frontmatter.acceptanceCriteria : null,
|
|
21990
|
+
technicalNotes: typeof t.frontmatter.technicalNotes === "string" ? t.frontmatter.technicalNotes : null,
|
|
21991
|
+
complexity: typeof t.frontmatter.complexity === "string" ? t.frontmatter.complexity : null,
|
|
21992
|
+
estimatedPoints: typeof t.frontmatter.estimatedPoints === "number" ? t.frontmatter.estimatedPoints : null,
|
|
21134
21993
|
priority: t.frontmatter.priority ?? null
|
|
21135
21994
|
})),
|
|
21136
21995
|
decisions: allDecisions.map((d) => ({
|
|
@@ -21953,6 +22812,24 @@ function createWebTools(store, projectName, navGroups) {
|
|
|
21953
22812
|
};
|
|
21954
22813
|
},
|
|
21955
22814
|
{ annotations: { readOnlyHint: true } }
|
|
22815
|
+
),
|
|
22816
|
+
tool22(
|
|
22817
|
+
"get_dashboard_sprint_summary",
|
|
22818
|
+
"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.",
|
|
22819
|
+
{
|
|
22820
|
+
sprint: external_exports.string().optional().describe("Sprint ID (e.g. 'SP-001'). Omit for the active sprint.")
|
|
22821
|
+
},
|
|
22822
|
+
async (args) => {
|
|
22823
|
+
const data = getSprintSummaryData(store, args.sprint);
|
|
22824
|
+
if (!data) {
|
|
22825
|
+
const msg = args.sprint ? `Sprint ${args.sprint} not found.` : "No active sprint found.";
|
|
22826
|
+
return { content: [{ type: "text", text: msg }], isError: true };
|
|
22827
|
+
}
|
|
22828
|
+
return {
|
|
22829
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
22830
|
+
};
|
|
22831
|
+
},
|
|
22832
|
+
{ annotations: { readOnlyHint: true } }
|
|
21956
22833
|
)
|
|
21957
22834
|
];
|
|
21958
22835
|
}
|
|
@@ -21978,11 +22855,11 @@ function createMarvinMcpServer(store, options) {
|
|
|
21978
22855
|
}
|
|
21979
22856
|
|
|
21980
22857
|
// src/agent/session-namer.ts
|
|
21981
|
-
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
22858
|
+
import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
|
|
21982
22859
|
async function generateSessionName(turns) {
|
|
21983
22860
|
try {
|
|
21984
22861
|
const transcript = turns.slice(-20).map((t) => `${t.role}: ${t.content.slice(0, 200)}`).join("\n");
|
|
21985
|
-
const result =
|
|
22862
|
+
const result = query2({
|
|
21986
22863
|
prompt: `Summarize this conversation in 3-5 words as a kebab-case name suitable for a filename. Output ONLY the name, nothing else.
|
|
21987
22864
|
|
|
21988
22865
|
${transcript}`,
|
|
@@ -22249,6 +23126,7 @@ Marvin \u2014 ${persona.name}
|
|
|
22249
23126
|
"mcp__marvin-governance__get_dashboard_gar",
|
|
22250
23127
|
"mcp__marvin-governance__get_dashboard_board",
|
|
22251
23128
|
"mcp__marvin-governance__get_dashboard_upcoming",
|
|
23129
|
+
"mcp__marvin-governance__get_dashboard_sprint_summary",
|
|
22252
23130
|
...pluginTools.map((t) => `mcp__marvin-governance__${t.name}`),
|
|
22253
23131
|
...codeSkillTools.map((t) => `mcp__marvin-governance__${t.name}`)
|
|
22254
23132
|
]
|
|
@@ -22259,7 +23137,7 @@ Marvin \u2014 ${persona.name}
|
|
|
22259
23137
|
if (existingSession) {
|
|
22260
23138
|
queryOptions.resume = existingSession.id;
|
|
22261
23139
|
}
|
|
22262
|
-
const conversation =
|
|
23140
|
+
const conversation = query3({
|
|
22263
23141
|
prompt,
|
|
22264
23142
|
options: queryOptions
|
|
22265
23143
|
});
|
|
@@ -22618,7 +23496,7 @@ import * as fs12 from "fs";
|
|
|
22618
23496
|
import * as path12 from "path";
|
|
22619
23497
|
import chalk7 from "chalk";
|
|
22620
23498
|
import ora2 from "ora";
|
|
22621
|
-
import { query as
|
|
23499
|
+
import { query as query4 } from "@anthropic-ai/claude-agent-sdk";
|
|
22622
23500
|
|
|
22623
23501
|
// src/sources/prompts.ts
|
|
22624
23502
|
function buildIngestSystemPrompt(persona, projectConfig, isDraft) {
|
|
@@ -22751,7 +23629,7 @@ async function ingestFile(options) {
|
|
|
22751
23629
|
const spinner = ora2({ text: `Analyzing ${fileName}...`, color: "cyan" });
|
|
22752
23630
|
spinner.start();
|
|
22753
23631
|
try {
|
|
22754
|
-
const conversation =
|
|
23632
|
+
const conversation = query4({
|
|
22755
23633
|
prompt: userPrompt,
|
|
22756
23634
|
options: {
|
|
22757
23635
|
systemPrompt,
|
|
@@ -23281,7 +24159,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
23281
24159
|
import { tool as tool23 } from "@anthropic-ai/claude-agent-sdk";
|
|
23282
24160
|
|
|
23283
24161
|
// src/skills/action-runner.ts
|
|
23284
|
-
import { query as
|
|
24162
|
+
import { query as query5 } from "@anthropic-ai/claude-agent-sdk";
|
|
23285
24163
|
var GOVERNANCE_TOOL_NAMES2 = [
|
|
23286
24164
|
"mcp__marvin-governance__list_decisions",
|
|
23287
24165
|
"mcp__marvin-governance__get_decision",
|
|
@@ -23303,7 +24181,7 @@ async function runSkillAction(action, userPrompt, context) {
|
|
|
23303
24181
|
try {
|
|
23304
24182
|
const mcpServer = createMarvinMcpServer(context.store);
|
|
23305
24183
|
const allowedTools = action.allowGovernanceTools !== false ? GOVERNANCE_TOOL_NAMES2 : [];
|
|
23306
|
-
const conversation =
|
|
24184
|
+
const conversation = query5({
|
|
23307
24185
|
prompt: userPrompt,
|
|
23308
24186
|
options: {
|
|
23309
24187
|
systemPrompt: action.systemPrompt,
|
|
@@ -24344,7 +25222,7 @@ import chalk13 from "chalk";
|
|
|
24344
25222
|
// src/analysis/analyze.ts
|
|
24345
25223
|
import chalk12 from "chalk";
|
|
24346
25224
|
import ora4 from "ora";
|
|
24347
|
-
import { query as
|
|
25225
|
+
import { query as query6 } from "@anthropic-ai/claude-agent-sdk";
|
|
24348
25226
|
|
|
24349
25227
|
// src/analysis/prompts.ts
|
|
24350
25228
|
function buildAnalyzeSystemPrompt(persona, projectConfig, isDraft) {
|
|
@@ -24474,7 +25352,7 @@ async function analyzeMeeting(options) {
|
|
|
24474
25352
|
const spinner = ora4({ text: `Analyzing meeting ${meetingId}...`, color: "cyan" });
|
|
24475
25353
|
spinner.start();
|
|
24476
25354
|
try {
|
|
24477
|
-
const conversation =
|
|
25355
|
+
const conversation = query6({
|
|
24478
25356
|
prompt: userPrompt,
|
|
24479
25357
|
options: {
|
|
24480
25358
|
systemPrompt,
|
|
@@ -24601,7 +25479,7 @@ import chalk15 from "chalk";
|
|
|
24601
25479
|
// src/contributions/contribute.ts
|
|
24602
25480
|
import chalk14 from "chalk";
|
|
24603
25481
|
import ora5 from "ora";
|
|
24604
|
-
import { query as
|
|
25482
|
+
import { query as query7 } from "@anthropic-ai/claude-agent-sdk";
|
|
24605
25483
|
|
|
24606
25484
|
// src/contributions/prompts.ts
|
|
24607
25485
|
function buildContributeSystemPrompt(persona, contributionType, projectConfig, isDraft) {
|
|
@@ -24855,7 +25733,7 @@ async function contributeFromPersona(options) {
|
|
|
24855
25733
|
"mcp__marvin-governance__get_action",
|
|
24856
25734
|
"mcp__marvin-governance__get_question"
|
|
24857
25735
|
];
|
|
24858
|
-
const conversation =
|
|
25736
|
+
const conversation = query7({
|
|
24859
25737
|
prompt: userPrompt,
|
|
24860
25738
|
options: {
|
|
24861
25739
|
systemPrompt,
|
|
@@ -25001,6 +25879,9 @@ Contribution: ${options.type}`));
|
|
|
25001
25879
|
});
|
|
25002
25880
|
}
|
|
25003
25881
|
|
|
25882
|
+
// src/cli/commands/report.ts
|
|
25883
|
+
import ora6 from "ora";
|
|
25884
|
+
|
|
25004
25885
|
// src/reports/gar/render-ascii.ts
|
|
25005
25886
|
import chalk16 from "chalk";
|
|
25006
25887
|
var STATUS_DOT = {
|
|
@@ -25195,6 +26076,47 @@ async function healthReportCommand(options) {
|
|
|
25195
26076
|
console.log(renderAscii2(report));
|
|
25196
26077
|
}
|
|
25197
26078
|
}
|
|
26079
|
+
async function sprintSummaryCommand(options) {
|
|
26080
|
+
const project = loadProject();
|
|
26081
|
+
const plugin = resolvePlugin(project.config.methodology);
|
|
26082
|
+
const pluginRegistrations = plugin?.documentTypeRegistrations ?? [];
|
|
26083
|
+
const allSkills = loadAllSkills(project.marvinDir);
|
|
26084
|
+
const allSkillIds = [...allSkills.keys()];
|
|
26085
|
+
const skillRegistrations = collectSkillRegistrations(allSkillIds, allSkills);
|
|
26086
|
+
const store = new DocumentStore(project.marvinDir, [...pluginRegistrations, ...skillRegistrations]);
|
|
26087
|
+
const data = collectSprintSummaryData(store, options.sprint);
|
|
26088
|
+
if (!data) {
|
|
26089
|
+
const msg = options.sprint ? `Sprint ${options.sprint} not found.` : "No active sprint found. Use --sprint <id> to specify one.";
|
|
26090
|
+
console.error(msg);
|
|
26091
|
+
process.exit(1);
|
|
26092
|
+
}
|
|
26093
|
+
const spinner = ora6({ text: "Generating AI sprint summary...", color: "cyan" }).start();
|
|
26094
|
+
try {
|
|
26095
|
+
const summary = await generateSprintSummary(data);
|
|
26096
|
+
spinner.stop();
|
|
26097
|
+
const header = `# Sprint Summary: ${data.sprint.id} \u2014 ${data.sprint.title}
|
|
26098
|
+
|
|
26099
|
+
`;
|
|
26100
|
+
console.log(header + summary);
|
|
26101
|
+
if (options.save) {
|
|
26102
|
+
const doc = store.create(
|
|
26103
|
+
"report",
|
|
26104
|
+
{
|
|
26105
|
+
title: `Sprint Summary: ${data.sprint.title}`,
|
|
26106
|
+
status: "final",
|
|
26107
|
+
tags: [`report-type:sprint-summary`, `sprint:${data.sprint.id}`]
|
|
26108
|
+
},
|
|
26109
|
+
summary
|
|
26110
|
+
);
|
|
26111
|
+
console.log(`
|
|
26112
|
+
Saved as ${doc.frontmatter.id}`);
|
|
26113
|
+
}
|
|
26114
|
+
} catch (err) {
|
|
26115
|
+
spinner.stop();
|
|
26116
|
+
console.error("Failed to generate sprint summary:", err);
|
|
26117
|
+
process.exit(1);
|
|
26118
|
+
}
|
|
26119
|
+
}
|
|
25198
26120
|
|
|
25199
26121
|
// src/cli/commands/web.ts
|
|
25200
26122
|
async function webCommand(options) {
|
|
@@ -25237,7 +26159,7 @@ function createProgram() {
|
|
|
25237
26159
|
const program2 = new Command();
|
|
25238
26160
|
program2.name("marvin").description(
|
|
25239
26161
|
"AI-powered product development assistant with Product Owner, Delivery Manager, and Technical Lead personas"
|
|
25240
|
-
).version("0.4.
|
|
26162
|
+
).version("0.4.7");
|
|
25241
26163
|
program2.command("init").description("Initialize a new Marvin project in the current directory").action(async () => {
|
|
25242
26164
|
await initCommand();
|
|
25243
26165
|
});
|
|
@@ -25320,6 +26242,9 @@ function createProgram() {
|
|
|
25320
26242
|
).action(async (options) => {
|
|
25321
26243
|
await healthReportCommand(options);
|
|
25322
26244
|
});
|
|
26245
|
+
reportCmd.command("sprint-summary").description("Generate an AI-powered sprint summary narrative").option("--sprint <id>", "Sprint ID (defaults to active sprint)").option("--save", "Save the summary as a report document").action(async (options) => {
|
|
26246
|
+
await sprintSummaryCommand(options);
|
|
26247
|
+
});
|
|
25323
26248
|
program2.command("web").description("Launch a local web dashboard for project data").option("-p, --port <port>", "Port to listen on (default: 3000)").option("--no-open", "Don't auto-open the browser").action(async (options) => {
|
|
25324
26249
|
await webCommand(options);
|
|
25325
26250
|
});
|