mrvn-cli 0.4.6 → 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 +767 -22
- package/dist/index.js.map +1 -1
- package/dist/marvin-serve.js +726 -31
- package/dist/marvin-serve.js.map +1 -1
- package/dist/marvin.js +785 -42
- 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,6 +18891,9 @@ 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
|
|
18557
18899
|
function collapsibleSection(sectionId, title, content, opts) {
|
|
@@ -18693,6 +19035,7 @@ function layout(opts, body) {
|
|
|
18693
19035
|
const topItems = [
|
|
18694
19036
|
{ href: "/", label: "Overview" },
|
|
18695
19037
|
{ href: "/upcoming", label: "Upcoming" },
|
|
19038
|
+
{ href: "/sprint-summary", label: "Sprint Summary" },
|
|
18696
19039
|
{ href: "/timeline", label: "Timeline" },
|
|
18697
19040
|
{ href: "/board", label: "Board" },
|
|
18698
19041
|
{ href: "/gar", label: "GAR Report" },
|
|
@@ -19703,6 +20046,112 @@ tr:hover td {
|
|
|
19703
20046
|
|
|
19704
20047
|
.text-dim { color: var(--text-dim); }
|
|
19705
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
|
+
|
|
19706
20155
|
/* Collapsible sections */
|
|
19707
20156
|
.collapsible-header {
|
|
19708
20157
|
cursor: pointer;
|
|
@@ -20594,7 +21043,199 @@ function upcomingPage(data) {
|
|
|
20594
21043
|
`;
|
|
20595
21044
|
}
|
|
20596
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
|
+
|
|
20597
21237
|
// src/web/router.ts
|
|
21238
|
+
var sprintSummaryCache = /* @__PURE__ */ new Map();
|
|
20598
21239
|
function handleRequest(req, res, store, projectName, navGroups) {
|
|
20599
21240
|
const parsed = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
20600
21241
|
const pathname = parsed.pathname;
|
|
@@ -20640,6 +21281,42 @@ function handleRequest(req, res, store, projectName, navGroups) {
|
|
|
20640
21281
|
respond(res, layout({ title: "Upcoming", activePath: "/upcoming", projectName, navGroups }, body));
|
|
20641
21282
|
return;
|
|
20642
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
|
+
}
|
|
20643
21320
|
const boardMatch = pathname.match(/^\/board(?:\/([^/]+))?$/);
|
|
20644
21321
|
if (boardMatch) {
|
|
20645
21322
|
const type = boardMatch[1];
|
|
@@ -22135,6 +22812,24 @@ function createWebTools(store, projectName, navGroups) {
|
|
|
22135
22812
|
};
|
|
22136
22813
|
},
|
|
22137
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 } }
|
|
22138
22833
|
)
|
|
22139
22834
|
];
|
|
22140
22835
|
}
|
|
@@ -22160,11 +22855,11 @@ function createMarvinMcpServer(store, options) {
|
|
|
22160
22855
|
}
|
|
22161
22856
|
|
|
22162
22857
|
// src/agent/session-namer.ts
|
|
22163
|
-
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
22858
|
+
import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
|
|
22164
22859
|
async function generateSessionName(turns) {
|
|
22165
22860
|
try {
|
|
22166
22861
|
const transcript = turns.slice(-20).map((t) => `${t.role}: ${t.content.slice(0, 200)}`).join("\n");
|
|
22167
|
-
const result =
|
|
22862
|
+
const result = query2({
|
|
22168
22863
|
prompt: `Summarize this conversation in 3-5 words as a kebab-case name suitable for a filename. Output ONLY the name, nothing else.
|
|
22169
22864
|
|
|
22170
22865
|
${transcript}`,
|
|
@@ -22431,6 +23126,7 @@ Marvin \u2014 ${persona.name}
|
|
|
22431
23126
|
"mcp__marvin-governance__get_dashboard_gar",
|
|
22432
23127
|
"mcp__marvin-governance__get_dashboard_board",
|
|
22433
23128
|
"mcp__marvin-governance__get_dashboard_upcoming",
|
|
23129
|
+
"mcp__marvin-governance__get_dashboard_sprint_summary",
|
|
22434
23130
|
...pluginTools.map((t) => `mcp__marvin-governance__${t.name}`),
|
|
22435
23131
|
...codeSkillTools.map((t) => `mcp__marvin-governance__${t.name}`)
|
|
22436
23132
|
]
|
|
@@ -22441,7 +23137,7 @@ Marvin \u2014 ${persona.name}
|
|
|
22441
23137
|
if (existingSession) {
|
|
22442
23138
|
queryOptions.resume = existingSession.id;
|
|
22443
23139
|
}
|
|
22444
|
-
const conversation =
|
|
23140
|
+
const conversation = query3({
|
|
22445
23141
|
prompt,
|
|
22446
23142
|
options: queryOptions
|
|
22447
23143
|
});
|
|
@@ -22800,7 +23496,7 @@ import * as fs12 from "fs";
|
|
|
22800
23496
|
import * as path12 from "path";
|
|
22801
23497
|
import chalk7 from "chalk";
|
|
22802
23498
|
import ora2 from "ora";
|
|
22803
|
-
import { query as
|
|
23499
|
+
import { query as query4 } from "@anthropic-ai/claude-agent-sdk";
|
|
22804
23500
|
|
|
22805
23501
|
// src/sources/prompts.ts
|
|
22806
23502
|
function buildIngestSystemPrompt(persona, projectConfig, isDraft) {
|
|
@@ -22933,7 +23629,7 @@ async function ingestFile(options) {
|
|
|
22933
23629
|
const spinner = ora2({ text: `Analyzing ${fileName}...`, color: "cyan" });
|
|
22934
23630
|
spinner.start();
|
|
22935
23631
|
try {
|
|
22936
|
-
const conversation =
|
|
23632
|
+
const conversation = query4({
|
|
22937
23633
|
prompt: userPrompt,
|
|
22938
23634
|
options: {
|
|
22939
23635
|
systemPrompt,
|
|
@@ -23463,7 +24159,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
23463
24159
|
import { tool as tool23 } from "@anthropic-ai/claude-agent-sdk";
|
|
23464
24160
|
|
|
23465
24161
|
// src/skills/action-runner.ts
|
|
23466
|
-
import { query as
|
|
24162
|
+
import { query as query5 } from "@anthropic-ai/claude-agent-sdk";
|
|
23467
24163
|
var GOVERNANCE_TOOL_NAMES2 = [
|
|
23468
24164
|
"mcp__marvin-governance__list_decisions",
|
|
23469
24165
|
"mcp__marvin-governance__get_decision",
|
|
@@ -23485,7 +24181,7 @@ async function runSkillAction(action, userPrompt, context) {
|
|
|
23485
24181
|
try {
|
|
23486
24182
|
const mcpServer = createMarvinMcpServer(context.store);
|
|
23487
24183
|
const allowedTools = action.allowGovernanceTools !== false ? GOVERNANCE_TOOL_NAMES2 : [];
|
|
23488
|
-
const conversation =
|
|
24184
|
+
const conversation = query5({
|
|
23489
24185
|
prompt: userPrompt,
|
|
23490
24186
|
options: {
|
|
23491
24187
|
systemPrompt: action.systemPrompt,
|
|
@@ -24526,7 +25222,7 @@ import chalk13 from "chalk";
|
|
|
24526
25222
|
// src/analysis/analyze.ts
|
|
24527
25223
|
import chalk12 from "chalk";
|
|
24528
25224
|
import ora4 from "ora";
|
|
24529
|
-
import { query as
|
|
25225
|
+
import { query as query6 } from "@anthropic-ai/claude-agent-sdk";
|
|
24530
25226
|
|
|
24531
25227
|
// src/analysis/prompts.ts
|
|
24532
25228
|
function buildAnalyzeSystemPrompt(persona, projectConfig, isDraft) {
|
|
@@ -24656,7 +25352,7 @@ async function analyzeMeeting(options) {
|
|
|
24656
25352
|
const spinner = ora4({ text: `Analyzing meeting ${meetingId}...`, color: "cyan" });
|
|
24657
25353
|
spinner.start();
|
|
24658
25354
|
try {
|
|
24659
|
-
const conversation =
|
|
25355
|
+
const conversation = query6({
|
|
24660
25356
|
prompt: userPrompt,
|
|
24661
25357
|
options: {
|
|
24662
25358
|
systemPrompt,
|
|
@@ -24783,7 +25479,7 @@ import chalk15 from "chalk";
|
|
|
24783
25479
|
// src/contributions/contribute.ts
|
|
24784
25480
|
import chalk14 from "chalk";
|
|
24785
25481
|
import ora5 from "ora";
|
|
24786
|
-
import { query as
|
|
25482
|
+
import { query as query7 } from "@anthropic-ai/claude-agent-sdk";
|
|
24787
25483
|
|
|
24788
25484
|
// src/contributions/prompts.ts
|
|
24789
25485
|
function buildContributeSystemPrompt(persona, contributionType, projectConfig, isDraft) {
|
|
@@ -25037,7 +25733,7 @@ async function contributeFromPersona(options) {
|
|
|
25037
25733
|
"mcp__marvin-governance__get_action",
|
|
25038
25734
|
"mcp__marvin-governance__get_question"
|
|
25039
25735
|
];
|
|
25040
|
-
const conversation =
|
|
25736
|
+
const conversation = query7({
|
|
25041
25737
|
prompt: userPrompt,
|
|
25042
25738
|
options: {
|
|
25043
25739
|
systemPrompt,
|
|
@@ -25183,6 +25879,9 @@ Contribution: ${options.type}`));
|
|
|
25183
25879
|
});
|
|
25184
25880
|
}
|
|
25185
25881
|
|
|
25882
|
+
// src/cli/commands/report.ts
|
|
25883
|
+
import ora6 from "ora";
|
|
25884
|
+
|
|
25186
25885
|
// src/reports/gar/render-ascii.ts
|
|
25187
25886
|
import chalk16 from "chalk";
|
|
25188
25887
|
var STATUS_DOT = {
|
|
@@ -25377,6 +26076,47 @@ async function healthReportCommand(options) {
|
|
|
25377
26076
|
console.log(renderAscii2(report));
|
|
25378
26077
|
}
|
|
25379
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
|
+
}
|
|
25380
26120
|
|
|
25381
26121
|
// src/cli/commands/web.ts
|
|
25382
26122
|
async function webCommand(options) {
|
|
@@ -25419,7 +26159,7 @@ function createProgram() {
|
|
|
25419
26159
|
const program2 = new Command();
|
|
25420
26160
|
program2.name("marvin").description(
|
|
25421
26161
|
"AI-powered product development assistant with Product Owner, Delivery Manager, and Technical Lead personas"
|
|
25422
|
-
).version("0.4.
|
|
26162
|
+
).version("0.4.7");
|
|
25423
26163
|
program2.command("init").description("Initialize a new Marvin project in the current directory").action(async () => {
|
|
25424
26164
|
await initCommand();
|
|
25425
26165
|
});
|
|
@@ -25502,6 +26242,9 @@ function createProgram() {
|
|
|
25502
26242
|
).action(async (options) => {
|
|
25503
26243
|
await healthReportCommand(options);
|
|
25504
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
|
+
});
|
|
25505
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) => {
|
|
25506
26249
|
await webCommand(options);
|
|
25507
26250
|
});
|