mrvn-cli 0.4.6 → 0.4.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -1
- package/dist/index.js +836 -26
- package/dist/index.js.map +1 -1
- package/dist/marvin-serve.js +795 -35
- package/dist/marvin-serve.js.map +1 -1
- package/dist/marvin.js +854 -46
- package/dist/marvin.js.map +1 -1
- package/package.json +1 -1
package/dist/marvin.js
CHANGED
|
@@ -14495,6 +14495,385 @@ 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 primaryDocs = workItemDocs.filter((d) => d.frontmatter.type !== "contribution");
|
|
14577
|
+
const byStatus = {};
|
|
14578
|
+
const byType = {};
|
|
14579
|
+
let doneCount = 0;
|
|
14580
|
+
let inProgressCount = 0;
|
|
14581
|
+
let openCount = 0;
|
|
14582
|
+
let blockedCount = 0;
|
|
14583
|
+
for (const doc of primaryDocs) {
|
|
14584
|
+
const s = doc.frontmatter.status;
|
|
14585
|
+
byStatus[s] = (byStatus[s] ?? 0) + 1;
|
|
14586
|
+
byType[doc.frontmatter.type] = (byType[doc.frontmatter.type] ?? 0) + 1;
|
|
14587
|
+
if (DONE_STATUSES.has(s)) doneCount++;
|
|
14588
|
+
else if (s === "in-progress") inProgressCount++;
|
|
14589
|
+
else if (s === "blocked") blockedCount++;
|
|
14590
|
+
else openCount++;
|
|
14591
|
+
}
|
|
14592
|
+
const allItemsById = /* @__PURE__ */ new Map();
|
|
14593
|
+
const childrenByParent = /* @__PURE__ */ new Map();
|
|
14594
|
+
const sprintItemIds = new Set(workItemDocs.map((d) => d.frontmatter.id));
|
|
14595
|
+
for (const doc of workItemDocs) {
|
|
14596
|
+
const about = doc.frontmatter.aboutArtifact;
|
|
14597
|
+
const item = {
|
|
14598
|
+
id: doc.frontmatter.id,
|
|
14599
|
+
title: doc.frontmatter.title,
|
|
14600
|
+
type: doc.frontmatter.type,
|
|
14601
|
+
status: doc.frontmatter.status,
|
|
14602
|
+
aboutArtifact: about
|
|
14603
|
+
};
|
|
14604
|
+
allItemsById.set(item.id, item);
|
|
14605
|
+
if (about && sprintItemIds.has(about)) {
|
|
14606
|
+
if (!childrenByParent.has(about)) childrenByParent.set(about, []);
|
|
14607
|
+
childrenByParent.get(about).push(item);
|
|
14608
|
+
}
|
|
14609
|
+
}
|
|
14610
|
+
const itemsWithChildren = /* @__PURE__ */ new Set();
|
|
14611
|
+
for (const [parentId, children] of childrenByParent) {
|
|
14612
|
+
const parent = allItemsById.get(parentId);
|
|
14613
|
+
if (parent) {
|
|
14614
|
+
parent.children = children;
|
|
14615
|
+
for (const child of children) itemsWithChildren.add(child.id);
|
|
14616
|
+
}
|
|
14617
|
+
}
|
|
14618
|
+
for (const item of allItemsById.values()) {
|
|
14619
|
+
if (item.children) {
|
|
14620
|
+
for (const child of item.children) {
|
|
14621
|
+
const grandchildren = childrenByParent.get(child.id);
|
|
14622
|
+
if (grandchildren) {
|
|
14623
|
+
child.children = grandchildren;
|
|
14624
|
+
for (const gc of grandchildren) itemsWithChildren.add(gc.id);
|
|
14625
|
+
}
|
|
14626
|
+
}
|
|
14627
|
+
}
|
|
14628
|
+
}
|
|
14629
|
+
const items = [];
|
|
14630
|
+
for (const doc of workItemDocs) {
|
|
14631
|
+
if (!itemsWithChildren.has(doc.frontmatter.id)) {
|
|
14632
|
+
items.push(allItemsById.get(doc.frontmatter.id));
|
|
14633
|
+
}
|
|
14634
|
+
}
|
|
14635
|
+
const workItems = {
|
|
14636
|
+
total: primaryDocs.length,
|
|
14637
|
+
done: doneCount,
|
|
14638
|
+
inProgress: inProgressCount,
|
|
14639
|
+
open: openCount,
|
|
14640
|
+
blocked: blockedCount,
|
|
14641
|
+
completionPct: primaryDocs.length > 0 ? Math.round(doneCount / primaryDocs.length * 100) : 0,
|
|
14642
|
+
byStatus,
|
|
14643
|
+
byType,
|
|
14644
|
+
items
|
|
14645
|
+
};
|
|
14646
|
+
const meetings = [];
|
|
14647
|
+
if (startDate && endDate) {
|
|
14648
|
+
const meetingDocs = allDocs.filter((d) => d.frontmatter.type === "meeting");
|
|
14649
|
+
for (const m of meetingDocs) {
|
|
14650
|
+
const meetingDate = m.frontmatter.date ?? m.frontmatter.created.slice(0, 10);
|
|
14651
|
+
if (meetingDate >= startDate && meetingDate <= endDate) {
|
|
14652
|
+
meetings.push({
|
|
14653
|
+
id: m.frontmatter.id,
|
|
14654
|
+
title: m.frontmatter.title,
|
|
14655
|
+
date: meetingDate
|
|
14656
|
+
});
|
|
14657
|
+
}
|
|
14658
|
+
}
|
|
14659
|
+
meetings.sort((a, b) => a.date.localeCompare(b.date));
|
|
14660
|
+
}
|
|
14661
|
+
const artifacts = [];
|
|
14662
|
+
if (startDate && endDate) {
|
|
14663
|
+
for (const doc of allDocs) {
|
|
14664
|
+
if (doc.frontmatter.type === "sprint") continue;
|
|
14665
|
+
const created = doc.frontmatter.created.slice(0, 10);
|
|
14666
|
+
const updated = doc.frontmatter.updated.slice(0, 10);
|
|
14667
|
+
if (created >= startDate && created <= endDate) {
|
|
14668
|
+
artifacts.push({
|
|
14669
|
+
id: doc.frontmatter.id,
|
|
14670
|
+
title: doc.frontmatter.title,
|
|
14671
|
+
type: doc.frontmatter.type,
|
|
14672
|
+
action: "created",
|
|
14673
|
+
date: created
|
|
14674
|
+
});
|
|
14675
|
+
} else if (updated >= startDate && updated <= endDate && updated !== created) {
|
|
14676
|
+
artifacts.push({
|
|
14677
|
+
id: doc.frontmatter.id,
|
|
14678
|
+
title: doc.frontmatter.title,
|
|
14679
|
+
type: doc.frontmatter.type,
|
|
14680
|
+
action: "updated",
|
|
14681
|
+
date: updated
|
|
14682
|
+
});
|
|
14683
|
+
}
|
|
14684
|
+
}
|
|
14685
|
+
artifacts.sort((a, b) => b.date.localeCompare(a.date));
|
|
14686
|
+
}
|
|
14687
|
+
const relevantTags = /* @__PURE__ */ new Set([sprintTag, ...linkedEpicIds.map((id) => `epic:${id}`)]);
|
|
14688
|
+
const openActions = allDocs.filter(
|
|
14689
|
+
(d) => d.frontmatter.type === "action" && !DONE_STATUSES.has(d.frontmatter.status) && d.frontmatter.tags?.some((t) => relevantTags.has(t))
|
|
14690
|
+
).map((d) => ({
|
|
14691
|
+
id: d.frontmatter.id,
|
|
14692
|
+
title: d.frontmatter.title,
|
|
14693
|
+
owner: d.frontmatter.owner,
|
|
14694
|
+
dueDate: d.frontmatter.dueDate
|
|
14695
|
+
}));
|
|
14696
|
+
const openQuestions = allDocs.filter(
|
|
14697
|
+
(d) => d.frontmatter.type === "question" && d.frontmatter.status === "open" && d.frontmatter.tags?.some((t) => relevantTags.has(t))
|
|
14698
|
+
).map((d) => ({
|
|
14699
|
+
id: d.frontmatter.id,
|
|
14700
|
+
title: d.frontmatter.title
|
|
14701
|
+
}));
|
|
14702
|
+
const blockers = allDocs.filter(
|
|
14703
|
+
(d) => d.frontmatter.status === "blocked" && d.frontmatter.tags?.includes(sprintTag)
|
|
14704
|
+
).map((d) => ({
|
|
14705
|
+
id: d.frontmatter.id,
|
|
14706
|
+
title: d.frontmatter.title,
|
|
14707
|
+
type: d.frontmatter.type
|
|
14708
|
+
}));
|
|
14709
|
+
const riskBlockers = allDocs.filter(
|
|
14710
|
+
(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)
|
|
14711
|
+
);
|
|
14712
|
+
for (const d of riskBlockers) {
|
|
14713
|
+
blockers.push({
|
|
14714
|
+
id: d.frontmatter.id,
|
|
14715
|
+
title: d.frontmatter.title,
|
|
14716
|
+
type: d.frontmatter.type
|
|
14717
|
+
});
|
|
14718
|
+
}
|
|
14719
|
+
let velocity = null;
|
|
14720
|
+
const currentRate = workItems.completionPct;
|
|
14721
|
+
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 ?? ""));
|
|
14722
|
+
if (completedSprints.length > 0) {
|
|
14723
|
+
const prev = completedSprints[0];
|
|
14724
|
+
const prevTag = `sprint:${prev.frontmatter.id}`;
|
|
14725
|
+
const prevWorkItems = allDocs.filter(
|
|
14726
|
+
(d) => d.frontmatter.type !== "sprint" && d.frontmatter.type !== "epic" && d.frontmatter.type !== "contribution" && d.frontmatter.tags?.includes(prevTag)
|
|
14727
|
+
);
|
|
14728
|
+
const prevDone = prevWorkItems.filter((d) => DONE_STATUSES.has(d.frontmatter.status)).length;
|
|
14729
|
+
const prevRate = prevWorkItems.length > 0 ? Math.round(prevDone / prevWorkItems.length * 100) : 0;
|
|
14730
|
+
velocity = {
|
|
14731
|
+
currentCompletionRate: currentRate,
|
|
14732
|
+
previousSprintRate: prevRate,
|
|
14733
|
+
previousSprintId: prev.frontmatter.id
|
|
14734
|
+
};
|
|
14735
|
+
} else {
|
|
14736
|
+
velocity = { currentCompletionRate: currentRate };
|
|
14737
|
+
}
|
|
14738
|
+
return {
|
|
14739
|
+
sprint: {
|
|
14740
|
+
id: fm.id,
|
|
14741
|
+
title: fm.title,
|
|
14742
|
+
goal: fm.goal,
|
|
14743
|
+
status: fm.status,
|
|
14744
|
+
startDate,
|
|
14745
|
+
endDate
|
|
14746
|
+
},
|
|
14747
|
+
timeline: { daysElapsed, daysRemaining, totalDays, percentComplete },
|
|
14748
|
+
linkedEpics,
|
|
14749
|
+
workItems,
|
|
14750
|
+
meetings,
|
|
14751
|
+
artifacts,
|
|
14752
|
+
openActions,
|
|
14753
|
+
openQuestions,
|
|
14754
|
+
blockers,
|
|
14755
|
+
velocity
|
|
14756
|
+
};
|
|
14757
|
+
}
|
|
14758
|
+
|
|
14759
|
+
// src/reports/sprint-summary/generator.ts
|
|
14760
|
+
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
14761
|
+
async function generateSprintSummary(data) {
|
|
14762
|
+
const prompt = buildPrompt(data);
|
|
14763
|
+
const result = query({
|
|
14764
|
+
prompt,
|
|
14765
|
+
options: {
|
|
14766
|
+
systemPrompt: SYSTEM_PROMPT,
|
|
14767
|
+
maxTurns: 1,
|
|
14768
|
+
tools: [],
|
|
14769
|
+
allowedTools: []
|
|
14770
|
+
}
|
|
14771
|
+
});
|
|
14772
|
+
for await (const msg of result) {
|
|
14773
|
+
if (msg.type === "assistant") {
|
|
14774
|
+
const text = msg.message.content.find(
|
|
14775
|
+
(b) => b.type === "text"
|
|
14776
|
+
);
|
|
14777
|
+
if (text) return text.text;
|
|
14778
|
+
}
|
|
14779
|
+
}
|
|
14780
|
+
return "Unable to generate sprint summary.";
|
|
14781
|
+
}
|
|
14782
|
+
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:
|
|
14783
|
+
|
|
14784
|
+
## Sprint Health
|
|
14785
|
+
One-line verdict on overall sprint health (healthy / at risk / behind).
|
|
14786
|
+
|
|
14787
|
+
## Goal Progress
|
|
14788
|
+
How close the team is to achieving the sprint goal. Reference the goal text and completion metrics.
|
|
14789
|
+
|
|
14790
|
+
## Key Achievements
|
|
14791
|
+
Notable completions, decisions made, meetings held during the sprint. Use bullet points.
|
|
14792
|
+
|
|
14793
|
+
## Current Risks
|
|
14794
|
+
Blockers, overdue items, unresolved questions, items without owners. Use bullet points. If none, say so.
|
|
14795
|
+
|
|
14796
|
+
## Outcome Projection
|
|
14797
|
+
Given the current pace and time remaining, what's the likely outcome? Will the sprint goal be met?
|
|
14798
|
+
|
|
14799
|
+
Be specific \u2014 reference artifact IDs, dates, and numbers from the data. Keep the tone professional but direct.`;
|
|
14800
|
+
function buildPrompt(data) {
|
|
14801
|
+
const sections = [];
|
|
14802
|
+
sections.push(`# Sprint: ${data.sprint.id} \u2014 ${data.sprint.title}`);
|
|
14803
|
+
sections.push(`Status: ${data.sprint.status}`);
|
|
14804
|
+
if (data.sprint.goal) sections.push(`Goal: ${data.sprint.goal}`);
|
|
14805
|
+
if (data.sprint.startDate) sections.push(`Start: ${data.sprint.startDate}`);
|
|
14806
|
+
if (data.sprint.endDate) sections.push(`End: ${data.sprint.endDate}`);
|
|
14807
|
+
sections.push(`
|
|
14808
|
+
## Timeline`);
|
|
14809
|
+
sections.push(`Days elapsed: ${data.timeline.daysElapsed} / ${data.timeline.totalDays}`);
|
|
14810
|
+
sections.push(`Days remaining: ${data.timeline.daysRemaining}`);
|
|
14811
|
+
sections.push(`Timeline progress: ${data.timeline.percentComplete}%`);
|
|
14812
|
+
sections.push(`
|
|
14813
|
+
## Work Items`);
|
|
14814
|
+
sections.push(`Total: ${data.workItems.total}, Done: ${data.workItems.done}, In Progress: ${data.workItems.inProgress}, Open: ${data.workItems.open}, Blocked: ${data.workItems.blocked}`);
|
|
14815
|
+
sections.push(`Completion: ${data.workItems.completionPct}%`);
|
|
14816
|
+
if (Object.keys(data.workItems.byType).length > 0) {
|
|
14817
|
+
sections.push(`By type: ${Object.entries(data.workItems.byType).map(([t, n]) => `${t}: ${n}`).join(", ")}`);
|
|
14818
|
+
}
|
|
14819
|
+
if (data.linkedEpics.length > 0) {
|
|
14820
|
+
sections.push(`
|
|
14821
|
+
## Linked Epics`);
|
|
14822
|
+
for (const e of data.linkedEpics) {
|
|
14823
|
+
sections.push(`- ${e.id}: ${e.title} [${e.status}] \u2014 ${e.tasksDone}/${e.tasksTotal} tasks done`);
|
|
14824
|
+
}
|
|
14825
|
+
}
|
|
14826
|
+
if (data.meetings.length > 0) {
|
|
14827
|
+
sections.push(`
|
|
14828
|
+
## Meetings During Sprint`);
|
|
14829
|
+
for (const m of data.meetings) {
|
|
14830
|
+
sections.push(`- ${m.date}: ${m.id} \u2014 ${m.title}`);
|
|
14831
|
+
}
|
|
14832
|
+
}
|
|
14833
|
+
if (data.artifacts.length > 0) {
|
|
14834
|
+
sections.push(`
|
|
14835
|
+
## Artifacts Created/Updated During Sprint`);
|
|
14836
|
+
for (const a of data.artifacts.slice(0, 20)) {
|
|
14837
|
+
sections.push(`- ${a.date}: ${a.id} (${a.type}) ${a.action} \u2014 ${a.title}`);
|
|
14838
|
+
}
|
|
14839
|
+
if (data.artifacts.length > 20) {
|
|
14840
|
+
sections.push(`... and ${data.artifacts.length - 20} more`);
|
|
14841
|
+
}
|
|
14842
|
+
}
|
|
14843
|
+
if (data.openActions.length > 0) {
|
|
14844
|
+
sections.push(`
|
|
14845
|
+
## Open Actions`);
|
|
14846
|
+
for (const a of data.openActions) {
|
|
14847
|
+
const owner = a.owner ?? "unowned";
|
|
14848
|
+
const due = a.dueDate ?? "no due date";
|
|
14849
|
+
sections.push(`- ${a.id}: ${a.title} (${owner}, ${due})`);
|
|
14850
|
+
}
|
|
14851
|
+
}
|
|
14852
|
+
if (data.openQuestions.length > 0) {
|
|
14853
|
+
sections.push(`
|
|
14854
|
+
## Open Questions`);
|
|
14855
|
+
for (const q of data.openQuestions) {
|
|
14856
|
+
sections.push(`- ${q.id}: ${q.title}`);
|
|
14857
|
+
}
|
|
14858
|
+
}
|
|
14859
|
+
if (data.blockers.length > 0) {
|
|
14860
|
+
sections.push(`
|
|
14861
|
+
## Blockers`);
|
|
14862
|
+
for (const b of data.blockers) {
|
|
14863
|
+
sections.push(`- ${b.id} (${b.type}): ${b.title}`);
|
|
14864
|
+
}
|
|
14865
|
+
}
|
|
14866
|
+
if (data.velocity) {
|
|
14867
|
+
sections.push(`
|
|
14868
|
+
## Velocity`);
|
|
14869
|
+
sections.push(`Current sprint completion rate: ${data.velocity.currentCompletionRate}%`);
|
|
14870
|
+
if (data.velocity.previousSprintRate !== void 0) {
|
|
14871
|
+
sections.push(`Previous sprint (${data.velocity.previousSprintId}): ${data.velocity.previousSprintRate}%`);
|
|
14872
|
+
}
|
|
14873
|
+
}
|
|
14874
|
+
return sections.join("\n");
|
|
14875
|
+
}
|
|
14876
|
+
|
|
14498
14877
|
// src/plugins/builtin/tools/reports.ts
|
|
14499
14878
|
function createReportTools(store) {
|
|
14500
14879
|
return [
|
|
@@ -14786,6 +15165,25 @@ function createReportTools(store) {
|
|
|
14786
15165
|
},
|
|
14787
15166
|
{ annotations: { readOnlyHint: true } }
|
|
14788
15167
|
),
|
|
15168
|
+
tool2(
|
|
15169
|
+
"generate_sprint_summary",
|
|
15170
|
+
"Generate an AI-powered narrative summary of a sprint's progress, health, achievements, risks, and projected outcome",
|
|
15171
|
+
{
|
|
15172
|
+
sprint: external_exports.string().optional().describe("Sprint ID (e.g. 'SP-001'). Omit for the active sprint.")
|
|
15173
|
+
},
|
|
15174
|
+
async (args) => {
|
|
15175
|
+
const data = collectSprintSummaryData(store, args.sprint);
|
|
15176
|
+
if (!data) {
|
|
15177
|
+
const msg = args.sprint ? `Sprint ${args.sprint} not found.` : "No active sprint found.";
|
|
15178
|
+
return { content: [{ type: "text", text: msg }], isError: true };
|
|
15179
|
+
}
|
|
15180
|
+
const summary = await generateSprintSummary(data);
|
|
15181
|
+
return {
|
|
15182
|
+
content: [{ type: "text", text: summary }]
|
|
15183
|
+
};
|
|
15184
|
+
},
|
|
15185
|
+
{ annotations: { readOnlyHint: true } }
|
|
15186
|
+
),
|
|
14789
15187
|
tool2(
|
|
14790
15188
|
"save_report",
|
|
14791
15189
|
"Save a generated report as a persistent document",
|
|
@@ -15218,18 +15616,18 @@ function createContributionTools(store) {
|
|
|
15218
15616
|
content: external_exports.string().describe("Contribution content \u2014 the input from the persona"),
|
|
15219
15617
|
persona: external_exports.string().describe("Persona making the contribution (e.g. 'tech-lead')"),
|
|
15220
15618
|
contributionType: external_exports.string().describe("Type of contribution (e.g. 'action-result', 'risk-finding')"),
|
|
15221
|
-
aboutArtifact: external_exports.string().
|
|
15222
|
-
status: external_exports.string().optional().describe("Status (default: '
|
|
15619
|
+
aboutArtifact: external_exports.string().describe("Artifact ID this contribution relates to (e.g. 'A-001', 'T-003')"),
|
|
15620
|
+
status: external_exports.string().optional().describe("Status (default: 'done')"),
|
|
15223
15621
|
tags: external_exports.array(external_exports.string()).optional().describe("Tags for categorization")
|
|
15224
15622
|
},
|
|
15225
15623
|
async (args) => {
|
|
15226
15624
|
const frontmatter = {
|
|
15227
15625
|
title: args.title,
|
|
15228
|
-
status: args.status ?? "
|
|
15626
|
+
status: args.status ?? "done",
|
|
15229
15627
|
persona: args.persona,
|
|
15230
15628
|
contributionType: args.contributionType
|
|
15231
15629
|
};
|
|
15232
|
-
|
|
15630
|
+
frontmatter.aboutArtifact = args.aboutArtifact;
|
|
15233
15631
|
if (args.tags) frontmatter.tags = args.tags;
|
|
15234
15632
|
const doc = store.create("contribution", frontmatter, args.content);
|
|
15235
15633
|
return {
|
|
@@ -15629,26 +16027,6 @@ function createSprintPlanningTools(store) {
|
|
|
15629
16027
|
|
|
15630
16028
|
// src/plugins/builtin/tools/tasks.ts
|
|
15631
16029
|
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
16030
|
var linkedEpicArray = external_exports.preprocess(
|
|
15653
16031
|
(val) => {
|
|
15654
16032
|
if (typeof val === "string") {
|
|
@@ -15733,6 +16111,7 @@ function createTaskTools(store) {
|
|
|
15733
16111
|
title: external_exports.string().describe("Task title"),
|
|
15734
16112
|
content: external_exports.string().describe("Task description and implementation details"),
|
|
15735
16113
|
linkedEpic: linkedEpicArray.describe("Epic ID(s) to link this task to (e.g. ['E-001'] or ['E-001', 'E-002'])"),
|
|
16114
|
+
aboutArtifact: external_exports.string().optional().describe("Parent artifact this task derives from (e.g. 'A-001')"),
|
|
15736
16115
|
status: external_exports.enum(["backlog", "ready", "in-progress", "review", "done"]).optional().describe("Task status (default: 'backlog')"),
|
|
15737
16116
|
acceptanceCriteria: external_exports.string().optional().describe("Acceptance criteria for the task"),
|
|
15738
16117
|
technicalNotes: external_exports.string().optional().describe("Technical implementation notes"),
|
|
@@ -15758,6 +16137,7 @@ function createTaskTools(store) {
|
|
|
15758
16137
|
linkedEpic: linkedEpics,
|
|
15759
16138
|
tags: [...generateEpicTags(linkedEpics), ...args.tags ?? []]
|
|
15760
16139
|
};
|
|
16140
|
+
if (args.aboutArtifact) frontmatter.aboutArtifact = args.aboutArtifact;
|
|
15761
16141
|
if (args.acceptanceCriteria) frontmatter.acceptanceCriteria = args.acceptanceCriteria;
|
|
15762
16142
|
if (args.technicalNotes) frontmatter.technicalNotes = args.technicalNotes;
|
|
15763
16143
|
if (args.estimatedPoints !== void 0) frontmatter.estimatedPoints = args.estimatedPoints;
|
|
@@ -15781,6 +16161,7 @@ function createTaskTools(store) {
|
|
|
15781
16161
|
{
|
|
15782
16162
|
id: external_exports.string().describe("Task ID to update"),
|
|
15783
16163
|
title: external_exports.string().optional().describe("New title"),
|
|
16164
|
+
aboutArtifact: external_exports.string().optional().describe("Parent artifact this task derives from (e.g. 'A-001')"),
|
|
15784
16165
|
status: external_exports.enum(["backlog", "ready", "in-progress", "review", "done"]).optional().describe("New status"),
|
|
15785
16166
|
content: external_exports.string().optional().describe("New content"),
|
|
15786
16167
|
linkedEpic: linkedEpicArray.optional().describe("New linked epic ID(s)"),
|
|
@@ -17243,7 +17624,7 @@ import * as readline from "readline";
|
|
|
17243
17624
|
import chalk2 from "chalk";
|
|
17244
17625
|
import ora from "ora";
|
|
17245
17626
|
import {
|
|
17246
|
-
query as
|
|
17627
|
+
query as query3
|
|
17247
17628
|
} from "@anthropic-ai/claude-agent-sdk";
|
|
17248
17629
|
|
|
17249
17630
|
// src/personas/prompt-builder.ts
|
|
@@ -17383,9 +17764,9 @@ var DocumentStore = class {
|
|
|
17383
17764
|
}
|
|
17384
17765
|
}
|
|
17385
17766
|
}
|
|
17386
|
-
list(
|
|
17767
|
+
list(query8) {
|
|
17387
17768
|
const results = [];
|
|
17388
|
-
const types =
|
|
17769
|
+
const types = query8?.type ? [query8.type] : Object.keys(this.typeDirs);
|
|
17389
17770
|
for (const type of types) {
|
|
17390
17771
|
const dirName = this.typeDirs[type];
|
|
17391
17772
|
if (!dirName) continue;
|
|
@@ -17396,9 +17777,9 @@ var DocumentStore = class {
|
|
|
17396
17777
|
const filePath = path6.join(dir, file2);
|
|
17397
17778
|
const raw = fs6.readFileSync(filePath, "utf-8");
|
|
17398
17779
|
const doc = parseDocument(raw, filePath);
|
|
17399
|
-
if (
|
|
17400
|
-
if (
|
|
17401
|
-
if (
|
|
17780
|
+
if (query8?.status && doc.frontmatter.status !== query8.status) continue;
|
|
17781
|
+
if (query8?.owner && doc.frontmatter.owner !== query8.owner) continue;
|
|
17782
|
+
if (query8?.tag && (!doc.frontmatter.tags || !doc.frontmatter.tags.includes(query8.tag)))
|
|
17402
17783
|
continue;
|
|
17403
17784
|
results.push(doc);
|
|
17404
17785
|
}
|
|
@@ -18364,7 +18745,7 @@ function computeUrgency(dueDateStr, todayStr) {
|
|
|
18364
18745
|
if (diffDays <= 14) return "upcoming";
|
|
18365
18746
|
return "later";
|
|
18366
18747
|
}
|
|
18367
|
-
var
|
|
18748
|
+
var DONE_STATUSES2 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
18368
18749
|
function getUpcomingData(store) {
|
|
18369
18750
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
18370
18751
|
const allDocs = store.list();
|
|
@@ -18373,7 +18754,7 @@ function getUpcomingData(store) {
|
|
|
18373
18754
|
docById.set(doc.frontmatter.id, doc);
|
|
18374
18755
|
}
|
|
18375
18756
|
const actions = allDocs.filter(
|
|
18376
|
-
(d) => d.frontmatter.type === "action" && !
|
|
18757
|
+
(d) => d.frontmatter.type === "action" && !DONE_STATUSES2.has(d.frontmatter.status)
|
|
18377
18758
|
);
|
|
18378
18759
|
const actionsWithDue = actions.filter((d) => d.frontmatter.dueDate);
|
|
18379
18760
|
const sprints = allDocs.filter((d) => d.frontmatter.type === "sprint");
|
|
@@ -18437,7 +18818,7 @@ function getUpcomingData(store) {
|
|
|
18437
18818
|
const sprintEnd = sprint.frontmatter.endDate;
|
|
18438
18819
|
const sprintTaskDocs = getSprintTasks(sprint);
|
|
18439
18820
|
for (const task of sprintTaskDocs) {
|
|
18440
|
-
if (
|
|
18821
|
+
if (DONE_STATUSES2.has(task.frontmatter.status)) continue;
|
|
18441
18822
|
const existing = taskSprintMap.get(task.frontmatter.id);
|
|
18442
18823
|
if (!existing || sprintEnd < existing.sprintEnd) {
|
|
18443
18824
|
taskSprintMap.set(task.frontmatter.id, { task, sprint, sprintEnd });
|
|
@@ -18454,7 +18835,7 @@ function getUpcomingData(store) {
|
|
|
18454
18835
|
urgency: computeUrgency(sprintEnd, today)
|
|
18455
18836
|
})).sort((a, b) => a.sprintEndDate.localeCompare(b.sprintEndDate));
|
|
18456
18837
|
const openItems = allDocs.filter(
|
|
18457
|
-
(d) => ["action", "question", "task"].includes(d.frontmatter.type) && !
|
|
18838
|
+
(d) => ["action", "question", "task"].includes(d.frontmatter.type) && !DONE_STATUSES2.has(d.frontmatter.status)
|
|
18458
18839
|
);
|
|
18459
18840
|
const fourteenDaysAgo = new Date(todayMs - fourteenDaysMs).toISOString().slice(0, 10);
|
|
18460
18841
|
const recentMeetings = allDocs.filter(
|
|
@@ -18552,6 +18933,9 @@ function getUpcomingData(store) {
|
|
|
18552
18933
|
}).filter((item) => item.score > 0).sort((a, b) => b.score - a.score).slice(0, 15);
|
|
18553
18934
|
return { dueSoonActions, dueSoonSprintTasks, trending };
|
|
18554
18935
|
}
|
|
18936
|
+
function getSprintSummaryData(store, sprintId) {
|
|
18937
|
+
return collectSprintSummaryData(store, sprintId);
|
|
18938
|
+
}
|
|
18555
18939
|
|
|
18556
18940
|
// src/web/templates/layout.ts
|
|
18557
18941
|
function collapsibleSection(sectionId, title, content, opts) {
|
|
@@ -18693,6 +19077,7 @@ function layout(opts, body) {
|
|
|
18693
19077
|
const topItems = [
|
|
18694
19078
|
{ href: "/", label: "Overview" },
|
|
18695
19079
|
{ href: "/upcoming", label: "Upcoming" },
|
|
19080
|
+
{ href: "/sprint-summary", label: "Sprint Summary" },
|
|
18696
19081
|
{ href: "/timeline", label: "Timeline" },
|
|
18697
19082
|
{ href: "/board", label: "Board" },
|
|
18698
19083
|
{ href: "/gar", label: "GAR Report" },
|
|
@@ -19076,6 +19461,17 @@ tr:hover td {
|
|
|
19076
19461
|
background: var(--bg-hover);
|
|
19077
19462
|
}
|
|
19078
19463
|
|
|
19464
|
+
/* Hierarchical work-item sub-rows */
|
|
19465
|
+
.child-row td {
|
|
19466
|
+
font-size: 0.8125rem;
|
|
19467
|
+
border-bottom-style: dashed;
|
|
19468
|
+
}
|
|
19469
|
+
.contribution-row td {
|
|
19470
|
+
font-size: 0.8125rem;
|
|
19471
|
+
color: var(--text-dim);
|
|
19472
|
+
border-bottom-style: dashed;
|
|
19473
|
+
}
|
|
19474
|
+
|
|
19079
19475
|
/* GAR */
|
|
19080
19476
|
.gar-overall {
|
|
19081
19477
|
text-align: center;
|
|
@@ -19703,6 +20099,112 @@ tr:hover td {
|
|
|
19703
20099
|
|
|
19704
20100
|
.text-dim { color: var(--text-dim); }
|
|
19705
20101
|
|
|
20102
|
+
/* Sprint Summary */
|
|
20103
|
+
.sprint-goal {
|
|
20104
|
+
background: var(--bg-card);
|
|
20105
|
+
border: 1px solid var(--border);
|
|
20106
|
+
border-radius: var(--radius);
|
|
20107
|
+
padding: 0.75rem 1rem;
|
|
20108
|
+
margin-bottom: 1rem;
|
|
20109
|
+
font-size: 0.9rem;
|
|
20110
|
+
color: var(--text);
|
|
20111
|
+
}
|
|
20112
|
+
|
|
20113
|
+
.sprint-progress-bar {
|
|
20114
|
+
position: relative;
|
|
20115
|
+
height: 24px;
|
|
20116
|
+
background: var(--bg-card);
|
|
20117
|
+
border: 1px solid var(--border);
|
|
20118
|
+
border-radius: 12px;
|
|
20119
|
+
margin-bottom: 1.25rem;
|
|
20120
|
+
overflow: hidden;
|
|
20121
|
+
}
|
|
20122
|
+
|
|
20123
|
+
.sprint-progress-fill {
|
|
20124
|
+
height: 100%;
|
|
20125
|
+
background: linear-gradient(90deg, var(--accent-dim), var(--accent));
|
|
20126
|
+
border-radius: 12px;
|
|
20127
|
+
transition: width 0.3s ease;
|
|
20128
|
+
}
|
|
20129
|
+
|
|
20130
|
+
.sprint-progress-label {
|
|
20131
|
+
position: absolute;
|
|
20132
|
+
top: 50%;
|
|
20133
|
+
left: 50%;
|
|
20134
|
+
transform: translate(-50%, -50%);
|
|
20135
|
+
font-size: 0.7rem;
|
|
20136
|
+
font-weight: 700;
|
|
20137
|
+
color: var(--text);
|
|
20138
|
+
}
|
|
20139
|
+
|
|
20140
|
+
.sprint-ai-section {
|
|
20141
|
+
margin-top: 2rem;
|
|
20142
|
+
background: var(--bg-card);
|
|
20143
|
+
border: 1px solid var(--border);
|
|
20144
|
+
border-radius: var(--radius);
|
|
20145
|
+
padding: 1.5rem;
|
|
20146
|
+
}
|
|
20147
|
+
|
|
20148
|
+
.sprint-ai-section h3 {
|
|
20149
|
+
font-size: 1rem;
|
|
20150
|
+
font-weight: 600;
|
|
20151
|
+
margin-bottom: 0.5rem;
|
|
20152
|
+
}
|
|
20153
|
+
|
|
20154
|
+
.sprint-generate-btn {
|
|
20155
|
+
background: var(--accent);
|
|
20156
|
+
color: #fff;
|
|
20157
|
+
border: none;
|
|
20158
|
+
border-radius: var(--radius);
|
|
20159
|
+
padding: 0.5rem 1.25rem;
|
|
20160
|
+
font-size: 0.85rem;
|
|
20161
|
+
font-weight: 600;
|
|
20162
|
+
cursor: pointer;
|
|
20163
|
+
margin-top: 0.75rem;
|
|
20164
|
+
transition: background 0.15s;
|
|
20165
|
+
}
|
|
20166
|
+
|
|
20167
|
+
.sprint-generate-btn:hover:not(:disabled) {
|
|
20168
|
+
background: var(--accent-dim);
|
|
20169
|
+
}
|
|
20170
|
+
|
|
20171
|
+
.sprint-generate-btn:disabled {
|
|
20172
|
+
opacity: 0.5;
|
|
20173
|
+
cursor: not-allowed;
|
|
20174
|
+
}
|
|
20175
|
+
|
|
20176
|
+
.sprint-loading {
|
|
20177
|
+
display: flex;
|
|
20178
|
+
align-items: center;
|
|
20179
|
+
gap: 0.75rem;
|
|
20180
|
+
padding: 1rem 0;
|
|
20181
|
+
color: var(--text-dim);
|
|
20182
|
+
font-size: 0.85rem;
|
|
20183
|
+
}
|
|
20184
|
+
|
|
20185
|
+
.sprint-spinner {
|
|
20186
|
+
width: 20px;
|
|
20187
|
+
height: 20px;
|
|
20188
|
+
border: 2px solid var(--border);
|
|
20189
|
+
border-top-color: var(--accent);
|
|
20190
|
+
border-radius: 50%;
|
|
20191
|
+
animation: sprint-spin 0.8s linear infinite;
|
|
20192
|
+
}
|
|
20193
|
+
|
|
20194
|
+
@keyframes sprint-spin {
|
|
20195
|
+
to { transform: rotate(360deg); }
|
|
20196
|
+
}
|
|
20197
|
+
|
|
20198
|
+
.sprint-error {
|
|
20199
|
+
color: var(--red);
|
|
20200
|
+
font-size: 0.85rem;
|
|
20201
|
+
padding: 0.5rem 0;
|
|
20202
|
+
}
|
|
20203
|
+
|
|
20204
|
+
.sprint-ai-section .detail-content {
|
|
20205
|
+
margin-top: 1rem;
|
|
20206
|
+
}
|
|
20207
|
+
|
|
19706
20208
|
/* Collapsible sections */
|
|
19707
20209
|
.collapsible-header {
|
|
19708
20210
|
cursor: pointer;
|
|
@@ -20594,7 +21096,211 @@ function upcomingPage(data) {
|
|
|
20594
21096
|
`;
|
|
20595
21097
|
}
|
|
20596
21098
|
|
|
21099
|
+
// src/web/templates/pages/sprint-summary.ts
|
|
21100
|
+
function progressBar(pct) {
|
|
21101
|
+
return `<div class="sprint-progress-bar">
|
|
21102
|
+
<div class="sprint-progress-fill" style="width: ${pct}%"></div>
|
|
21103
|
+
<span class="sprint-progress-label">${pct}%</span>
|
|
21104
|
+
</div>`;
|
|
21105
|
+
}
|
|
21106
|
+
function sprintSummaryPage(data, cached2) {
|
|
21107
|
+
if (!data) {
|
|
21108
|
+
return `
|
|
21109
|
+
<div class="page-header">
|
|
21110
|
+
<h2>Sprint Summary</h2>
|
|
21111
|
+
<div class="subtitle">AI-powered sprint narrative</div>
|
|
21112
|
+
</div>
|
|
21113
|
+
<div class="empty">
|
|
21114
|
+
<h3>No Active Sprint</h3>
|
|
21115
|
+
<p>No active sprint found. Create a sprint and set its status to "active" to see the summary.</p>
|
|
21116
|
+
</div>`;
|
|
21117
|
+
}
|
|
21118
|
+
const statsCards = `
|
|
21119
|
+
<div class="cards">
|
|
21120
|
+
<div class="card">
|
|
21121
|
+
<div class="card-label">Completion</div>
|
|
21122
|
+
<div class="card-value">${data.workItems.completionPct}%</div>
|
|
21123
|
+
<div class="card-sub">${data.workItems.done} / ${data.workItems.total} items done</div>
|
|
21124
|
+
</div>
|
|
21125
|
+
<div class="card">
|
|
21126
|
+
<div class="card-label">Days Remaining</div>
|
|
21127
|
+
<div class="card-value">${data.timeline.daysRemaining}</div>
|
|
21128
|
+
<div class="card-sub">${data.timeline.daysElapsed} of ${data.timeline.totalDays} elapsed</div>
|
|
21129
|
+
</div>
|
|
21130
|
+
<div class="card">
|
|
21131
|
+
<div class="card-label">Epics</div>
|
|
21132
|
+
<div class="card-value">${data.linkedEpics.length}</div>
|
|
21133
|
+
<div class="card-sub">linked to sprint</div>
|
|
21134
|
+
</div>
|
|
21135
|
+
<div class="card">
|
|
21136
|
+
<div class="card-label">Blockers</div>
|
|
21137
|
+
<div class="card-value${data.blockers.length > 0 ? " priority-high" : ""}">${data.blockers.length}</div>
|
|
21138
|
+
<div class="card-sub">${data.openActions.length} open actions</div>
|
|
21139
|
+
</div>
|
|
21140
|
+
</div>`;
|
|
21141
|
+
const epicsTable = data.linkedEpics.length > 0 ? collapsibleSection(
|
|
21142
|
+
"ss-epics",
|
|
21143
|
+
"Linked Epics",
|
|
21144
|
+
`<div class="table-wrap">
|
|
21145
|
+
<table>
|
|
21146
|
+
<thead>
|
|
21147
|
+
<tr><th>ID</th><th>Title</th><th>Status</th><th>Tasks</th></tr>
|
|
21148
|
+
</thead>
|
|
21149
|
+
<tbody>
|
|
21150
|
+
${data.linkedEpics.map((e) => `
|
|
21151
|
+
<tr>
|
|
21152
|
+
<td><a href="/docs/epic/${escapeHtml(e.id)}">${escapeHtml(e.id)}</a></td>
|
|
21153
|
+
<td>${escapeHtml(e.title)}</td>
|
|
21154
|
+
<td>${statusBadge(e.status)}</td>
|
|
21155
|
+
<td>${e.tasksDone} / ${e.tasksTotal}</td>
|
|
21156
|
+
</tr>`).join("")}
|
|
21157
|
+
</tbody>
|
|
21158
|
+
</table>
|
|
21159
|
+
</div>`,
|
|
21160
|
+
{ titleTag: "h3" }
|
|
21161
|
+
) : "";
|
|
21162
|
+
function renderItemRows(items, depth = 0) {
|
|
21163
|
+
return items.flatMap((w) => {
|
|
21164
|
+
const isChild = depth > 0;
|
|
21165
|
+
const isContribution = w.type === "contribution";
|
|
21166
|
+
const rowClass = isContribution ? ' class="contribution-row"' : isChild ? ' class="child-row"' : "";
|
|
21167
|
+
const indent = depth > 0 ? ` style="padding-left: ${0.75 + depth * 1}rem"` : "";
|
|
21168
|
+
const row = `
|
|
21169
|
+
<tr${rowClass}>
|
|
21170
|
+
<td${indent}><a href="/docs/${escapeHtml(w.type)}/${escapeHtml(w.id)}">${escapeHtml(w.id)}</a></td>
|
|
21171
|
+
<td>${escapeHtml(w.title)}</td>
|
|
21172
|
+
<td>${escapeHtml(typeLabel(w.type))}</td>
|
|
21173
|
+
<td>${statusBadge(w.status)}</td>
|
|
21174
|
+
</tr>`;
|
|
21175
|
+
const childRows = w.children ? renderItemRows(w.children, depth + 1) : [];
|
|
21176
|
+
return [row, ...childRows];
|
|
21177
|
+
});
|
|
21178
|
+
}
|
|
21179
|
+
const workItemRows = renderItemRows(data.workItems.items);
|
|
21180
|
+
const workItemsSection = workItemRows.length > 0 ? collapsibleSection(
|
|
21181
|
+
"ss-work-items",
|
|
21182
|
+
"Work Items",
|
|
21183
|
+
`<div class="table-wrap">
|
|
21184
|
+
<table>
|
|
21185
|
+
<thead>
|
|
21186
|
+
<tr><th>ID</th><th>Title</th><th>Type</th><th>Status</th></tr>
|
|
21187
|
+
</thead>
|
|
21188
|
+
<tbody>
|
|
21189
|
+
${workItemRows.join("")}
|
|
21190
|
+
</tbody>
|
|
21191
|
+
</table>
|
|
21192
|
+
</div>`,
|
|
21193
|
+
{ titleTag: "h3", defaultCollapsed: true }
|
|
21194
|
+
) : "";
|
|
21195
|
+
const activitySection = data.artifacts.length > 0 ? collapsibleSection(
|
|
21196
|
+
"ss-activity",
|
|
21197
|
+
"Recent Activity",
|
|
21198
|
+
`<div class="table-wrap">
|
|
21199
|
+
<table>
|
|
21200
|
+
<thead>
|
|
21201
|
+
<tr><th>Date</th><th>ID</th><th>Title</th><th>Type</th><th>Action</th></tr>
|
|
21202
|
+
</thead>
|
|
21203
|
+
<tbody>
|
|
21204
|
+
${data.artifacts.slice(0, 15).map((a) => `
|
|
21205
|
+
<tr>
|
|
21206
|
+
<td>${formatDate(a.date)}</td>
|
|
21207
|
+
<td><a href="/docs/${escapeHtml(a.type)}/${escapeHtml(a.id)}">${escapeHtml(a.id)}</a></td>
|
|
21208
|
+
<td>${escapeHtml(a.title)}</td>
|
|
21209
|
+
<td>${escapeHtml(typeLabel(a.type))}</td>
|
|
21210
|
+
<td>${escapeHtml(a.action)}</td>
|
|
21211
|
+
</tr>`).join("")}
|
|
21212
|
+
</tbody>
|
|
21213
|
+
</table>
|
|
21214
|
+
</div>`,
|
|
21215
|
+
{ titleTag: "h3", defaultCollapsed: true }
|
|
21216
|
+
) : "";
|
|
21217
|
+
const meetingsSection = data.meetings.length > 0 ? collapsibleSection(
|
|
21218
|
+
"ss-meetings",
|
|
21219
|
+
`Meetings (${data.meetings.length})`,
|
|
21220
|
+
`<div class="table-wrap">
|
|
21221
|
+
<table>
|
|
21222
|
+
<thead>
|
|
21223
|
+
<tr><th>Date</th><th>ID</th><th>Title</th></tr>
|
|
21224
|
+
</thead>
|
|
21225
|
+
<tbody>
|
|
21226
|
+
${data.meetings.map((m) => `
|
|
21227
|
+
<tr>
|
|
21228
|
+
<td>${formatDate(m.date)}</td>
|
|
21229
|
+
<td><a href="/docs/meeting/${escapeHtml(m.id)}">${escapeHtml(m.id)}</a></td>
|
|
21230
|
+
<td>${escapeHtml(m.title)}</td>
|
|
21231
|
+
</tr>`).join("")}
|
|
21232
|
+
</tbody>
|
|
21233
|
+
</table>
|
|
21234
|
+
</div>`,
|
|
21235
|
+
{ titleTag: "h3", defaultCollapsed: true }
|
|
21236
|
+
) : "";
|
|
21237
|
+
const goalHtml = data.sprint.goal ? `<div class="sprint-goal"><strong>Goal:</strong> ${escapeHtml(data.sprint.goal)}</div>` : "";
|
|
21238
|
+
const dateRange = data.sprint.startDate && data.sprint.endDate ? `<span class="text-dim">${formatDate(data.sprint.startDate)} \u2014 ${formatDate(data.sprint.endDate)}</span>` : "";
|
|
21239
|
+
return `
|
|
21240
|
+
<div class="page-header">
|
|
21241
|
+
<h2>${escapeHtml(data.sprint.id)} \u2014 ${escapeHtml(data.sprint.title)} ${statusBadge(data.sprint.status)}</h2>
|
|
21242
|
+
<div class="subtitle">Sprint Summary ${dateRange}</div>
|
|
21243
|
+
</div>
|
|
21244
|
+
${goalHtml}
|
|
21245
|
+
${progressBar(data.timeline.percentComplete)}
|
|
21246
|
+
${statsCards}
|
|
21247
|
+
${epicsTable}
|
|
21248
|
+
${workItemsSection}
|
|
21249
|
+
${activitySection}
|
|
21250
|
+
${meetingsSection}
|
|
21251
|
+
|
|
21252
|
+
<div class="sprint-ai-section">
|
|
21253
|
+
<h3>AI Summary</h3>
|
|
21254
|
+
${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>`}
|
|
21255
|
+
<button class="sprint-generate-btn" onclick="generateSummary()" id="generate-btn">${cached2 ? "Regenerate" : "Generate AI Summary"}</button>
|
|
21256
|
+
<div id="summary-loading" class="sprint-loading" style="display:none">
|
|
21257
|
+
<div class="sprint-spinner"></div>
|
|
21258
|
+
<span>Generating summary...</span>
|
|
21259
|
+
</div>
|
|
21260
|
+
<div id="summary-error" class="sprint-error" style="display:none"></div>
|
|
21261
|
+
<div id="summary-content" class="detail-content"${cached2 ? "" : ' style="display:none"'}>${cached2 ? cached2.html : ""}</div>
|
|
21262
|
+
</div>
|
|
21263
|
+
|
|
21264
|
+
<script>
|
|
21265
|
+
async function generateSummary() {
|
|
21266
|
+
var btn = document.getElementById('generate-btn');
|
|
21267
|
+
var loading = document.getElementById('summary-loading');
|
|
21268
|
+
var errorEl = document.getElementById('summary-error');
|
|
21269
|
+
var content = document.getElementById('summary-content');
|
|
21270
|
+
|
|
21271
|
+
btn.disabled = true;
|
|
21272
|
+
btn.style.display = 'none';
|
|
21273
|
+
loading.style.display = 'flex';
|
|
21274
|
+
errorEl.style.display = 'none';
|
|
21275
|
+
content.style.display = 'none';
|
|
21276
|
+
|
|
21277
|
+
try {
|
|
21278
|
+
var res = await fetch('/api/sprint-summary', {
|
|
21279
|
+
method: 'POST',
|
|
21280
|
+
headers: { 'Content-Type': 'application/json' },
|
|
21281
|
+
body: JSON.stringify({ sprintId: '${escapeHtml(data.sprint.id)}' })
|
|
21282
|
+
});
|
|
21283
|
+
var json = await res.json();
|
|
21284
|
+
if (!res.ok) throw new Error(json.error || 'Failed to generate summary');
|
|
21285
|
+
loading.style.display = 'none';
|
|
21286
|
+
content.innerHTML = json.html;
|
|
21287
|
+
content.style.display = 'block';
|
|
21288
|
+
btn.textContent = 'Regenerate';
|
|
21289
|
+
btn.style.display = '';
|
|
21290
|
+
btn.disabled = false;
|
|
21291
|
+
} catch (e) {
|
|
21292
|
+
loading.style.display = 'none';
|
|
21293
|
+
errorEl.textContent = e.message;
|
|
21294
|
+
errorEl.style.display = 'block';
|
|
21295
|
+
btn.style.display = '';
|
|
21296
|
+
btn.disabled = false;
|
|
21297
|
+
}
|
|
21298
|
+
}
|
|
21299
|
+
</script>`;
|
|
21300
|
+
}
|
|
21301
|
+
|
|
20597
21302
|
// src/web/router.ts
|
|
21303
|
+
var sprintSummaryCache = /* @__PURE__ */ new Map();
|
|
20598
21304
|
function handleRequest(req, res, store, projectName, navGroups) {
|
|
20599
21305
|
const parsed = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
20600
21306
|
const pathname = parsed.pathname;
|
|
@@ -20640,6 +21346,42 @@ function handleRequest(req, res, store, projectName, navGroups) {
|
|
|
20640
21346
|
respond(res, layout({ title: "Upcoming", activePath: "/upcoming", projectName, navGroups }, body));
|
|
20641
21347
|
return;
|
|
20642
21348
|
}
|
|
21349
|
+
if (pathname === "/sprint-summary" && req.method === "GET") {
|
|
21350
|
+
const sprintId = parsed.searchParams.get("sprint") ?? void 0;
|
|
21351
|
+
const data = getSprintSummaryData(store, sprintId);
|
|
21352
|
+
const cached2 = data ? sprintSummaryCache.get(data.sprint.id) : void 0;
|
|
21353
|
+
const body = sprintSummaryPage(data, cached2 ? { html: cached2.html, generatedAt: cached2.generatedAt } : void 0);
|
|
21354
|
+
respond(res, layout({ title: "Sprint Summary", activePath: "/sprint-summary", projectName, navGroups }, body));
|
|
21355
|
+
return;
|
|
21356
|
+
}
|
|
21357
|
+
if (pathname === "/api/sprint-summary" && req.method === "POST") {
|
|
21358
|
+
let bodyStr = "";
|
|
21359
|
+
req.on("data", (chunk) => {
|
|
21360
|
+
bodyStr += chunk;
|
|
21361
|
+
});
|
|
21362
|
+
req.on("end", async () => {
|
|
21363
|
+
try {
|
|
21364
|
+
const { sprintId } = JSON.parse(bodyStr || "{}");
|
|
21365
|
+
const data = getSprintSummaryData(store, sprintId);
|
|
21366
|
+
if (!data) {
|
|
21367
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
21368
|
+
res.end(JSON.stringify({ error: "Sprint not found" }));
|
|
21369
|
+
return;
|
|
21370
|
+
}
|
|
21371
|
+
const summary = await generateSprintSummary(data);
|
|
21372
|
+
const html = renderMarkdown(summary);
|
|
21373
|
+
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
21374
|
+
sprintSummaryCache.set(data.sprint.id, { html, generatedAt });
|
|
21375
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
21376
|
+
res.end(JSON.stringify({ summary, html, generatedAt }));
|
|
21377
|
+
} catch (err) {
|
|
21378
|
+
console.error("[marvin web] Sprint summary generation error:", err);
|
|
21379
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
21380
|
+
res.end(JSON.stringify({ error: "Failed to generate summary" }));
|
|
21381
|
+
}
|
|
21382
|
+
});
|
|
21383
|
+
return;
|
|
21384
|
+
}
|
|
20643
21385
|
const boardMatch = pathname.match(/^\/board(?:\/([^/]+))?$/);
|
|
20644
21386
|
if (boardMatch) {
|
|
20645
21387
|
const type = boardMatch[1];
|
|
@@ -22135,6 +22877,24 @@ function createWebTools(store, projectName, navGroups) {
|
|
|
22135
22877
|
};
|
|
22136
22878
|
},
|
|
22137
22879
|
{ annotations: { readOnlyHint: true } }
|
|
22880
|
+
),
|
|
22881
|
+
tool22(
|
|
22882
|
+
"get_dashboard_sprint_summary",
|
|
22883
|
+
"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.",
|
|
22884
|
+
{
|
|
22885
|
+
sprint: external_exports.string().optional().describe("Sprint ID (e.g. 'SP-001'). Omit for the active sprint.")
|
|
22886
|
+
},
|
|
22887
|
+
async (args) => {
|
|
22888
|
+
const data = getSprintSummaryData(store, args.sprint);
|
|
22889
|
+
if (!data) {
|
|
22890
|
+
const msg = args.sprint ? `Sprint ${args.sprint} not found.` : "No active sprint found.";
|
|
22891
|
+
return { content: [{ type: "text", text: msg }], isError: true };
|
|
22892
|
+
}
|
|
22893
|
+
return {
|
|
22894
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
22895
|
+
};
|
|
22896
|
+
},
|
|
22897
|
+
{ annotations: { readOnlyHint: true } }
|
|
22138
22898
|
)
|
|
22139
22899
|
];
|
|
22140
22900
|
}
|
|
@@ -22160,11 +22920,11 @@ function createMarvinMcpServer(store, options) {
|
|
|
22160
22920
|
}
|
|
22161
22921
|
|
|
22162
22922
|
// src/agent/session-namer.ts
|
|
22163
|
-
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
22923
|
+
import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
|
|
22164
22924
|
async function generateSessionName(turns) {
|
|
22165
22925
|
try {
|
|
22166
22926
|
const transcript = turns.slice(-20).map((t) => `${t.role}: ${t.content.slice(0, 200)}`).join("\n");
|
|
22167
|
-
const result =
|
|
22927
|
+
const result = query2({
|
|
22168
22928
|
prompt: `Summarize this conversation in 3-5 words as a kebab-case name suitable for a filename. Output ONLY the name, nothing else.
|
|
22169
22929
|
|
|
22170
22930
|
${transcript}`,
|
|
@@ -22431,6 +23191,7 @@ Marvin \u2014 ${persona.name}
|
|
|
22431
23191
|
"mcp__marvin-governance__get_dashboard_gar",
|
|
22432
23192
|
"mcp__marvin-governance__get_dashboard_board",
|
|
22433
23193
|
"mcp__marvin-governance__get_dashboard_upcoming",
|
|
23194
|
+
"mcp__marvin-governance__get_dashboard_sprint_summary",
|
|
22434
23195
|
...pluginTools.map((t) => `mcp__marvin-governance__${t.name}`),
|
|
22435
23196
|
...codeSkillTools.map((t) => `mcp__marvin-governance__${t.name}`)
|
|
22436
23197
|
]
|
|
@@ -22441,7 +23202,7 @@ Marvin \u2014 ${persona.name}
|
|
|
22441
23202
|
if (existingSession) {
|
|
22442
23203
|
queryOptions.resume = existingSession.id;
|
|
22443
23204
|
}
|
|
22444
|
-
const conversation =
|
|
23205
|
+
const conversation = query3({
|
|
22445
23206
|
prompt,
|
|
22446
23207
|
options: queryOptions
|
|
22447
23208
|
});
|
|
@@ -22800,7 +23561,7 @@ import * as fs12 from "fs";
|
|
|
22800
23561
|
import * as path12 from "path";
|
|
22801
23562
|
import chalk7 from "chalk";
|
|
22802
23563
|
import ora2 from "ora";
|
|
22803
|
-
import { query as
|
|
23564
|
+
import { query as query4 } from "@anthropic-ai/claude-agent-sdk";
|
|
22804
23565
|
|
|
22805
23566
|
// src/sources/prompts.ts
|
|
22806
23567
|
function buildIngestSystemPrompt(persona, projectConfig, isDraft) {
|
|
@@ -22933,7 +23694,7 @@ async function ingestFile(options) {
|
|
|
22933
23694
|
const spinner = ora2({ text: `Analyzing ${fileName}...`, color: "cyan" });
|
|
22934
23695
|
spinner.start();
|
|
22935
23696
|
try {
|
|
22936
|
-
const conversation =
|
|
23697
|
+
const conversation = query4({
|
|
22937
23698
|
prompt: userPrompt,
|
|
22938
23699
|
options: {
|
|
22939
23700
|
systemPrompt,
|
|
@@ -23463,7 +24224,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
23463
24224
|
import { tool as tool23 } from "@anthropic-ai/claude-agent-sdk";
|
|
23464
24225
|
|
|
23465
24226
|
// src/skills/action-runner.ts
|
|
23466
|
-
import { query as
|
|
24227
|
+
import { query as query5 } from "@anthropic-ai/claude-agent-sdk";
|
|
23467
24228
|
var GOVERNANCE_TOOL_NAMES2 = [
|
|
23468
24229
|
"mcp__marvin-governance__list_decisions",
|
|
23469
24230
|
"mcp__marvin-governance__get_decision",
|
|
@@ -23485,7 +24246,7 @@ async function runSkillAction(action, userPrompt, context) {
|
|
|
23485
24246
|
try {
|
|
23486
24247
|
const mcpServer = createMarvinMcpServer(context.store);
|
|
23487
24248
|
const allowedTools = action.allowGovernanceTools !== false ? GOVERNANCE_TOOL_NAMES2 : [];
|
|
23488
|
-
const conversation =
|
|
24249
|
+
const conversation = query5({
|
|
23489
24250
|
prompt: userPrompt,
|
|
23490
24251
|
options: {
|
|
23491
24252
|
systemPrompt: action.systemPrompt,
|
|
@@ -24526,7 +25287,7 @@ import chalk13 from "chalk";
|
|
|
24526
25287
|
// src/analysis/analyze.ts
|
|
24527
25288
|
import chalk12 from "chalk";
|
|
24528
25289
|
import ora4 from "ora";
|
|
24529
|
-
import { query as
|
|
25290
|
+
import { query as query6 } from "@anthropic-ai/claude-agent-sdk";
|
|
24530
25291
|
|
|
24531
25292
|
// src/analysis/prompts.ts
|
|
24532
25293
|
function buildAnalyzeSystemPrompt(persona, projectConfig, isDraft) {
|
|
@@ -24656,7 +25417,7 @@ async function analyzeMeeting(options) {
|
|
|
24656
25417
|
const spinner = ora4({ text: `Analyzing meeting ${meetingId}...`, color: "cyan" });
|
|
24657
25418
|
spinner.start();
|
|
24658
25419
|
try {
|
|
24659
|
-
const conversation =
|
|
25420
|
+
const conversation = query6({
|
|
24660
25421
|
prompt: userPrompt,
|
|
24661
25422
|
options: {
|
|
24662
25423
|
systemPrompt,
|
|
@@ -24783,7 +25544,7 @@ import chalk15 from "chalk";
|
|
|
24783
25544
|
// src/contributions/contribute.ts
|
|
24784
25545
|
import chalk14 from "chalk";
|
|
24785
25546
|
import ora5 from "ora";
|
|
24786
|
-
import { query as
|
|
25547
|
+
import { query as query7 } from "@anthropic-ai/claude-agent-sdk";
|
|
24787
25548
|
|
|
24788
25549
|
// src/contributions/prompts.ts
|
|
24789
25550
|
function buildContributeSystemPrompt(persona, contributionType, projectConfig, isDraft) {
|
|
@@ -25037,7 +25798,7 @@ async function contributeFromPersona(options) {
|
|
|
25037
25798
|
"mcp__marvin-governance__get_action",
|
|
25038
25799
|
"mcp__marvin-governance__get_question"
|
|
25039
25800
|
];
|
|
25040
|
-
const conversation =
|
|
25801
|
+
const conversation = query7({
|
|
25041
25802
|
prompt: userPrompt,
|
|
25042
25803
|
options: {
|
|
25043
25804
|
systemPrompt,
|
|
@@ -25183,6 +25944,9 @@ Contribution: ${options.type}`));
|
|
|
25183
25944
|
});
|
|
25184
25945
|
}
|
|
25185
25946
|
|
|
25947
|
+
// src/cli/commands/report.ts
|
|
25948
|
+
import ora6 from "ora";
|
|
25949
|
+
|
|
25186
25950
|
// src/reports/gar/render-ascii.ts
|
|
25187
25951
|
import chalk16 from "chalk";
|
|
25188
25952
|
var STATUS_DOT = {
|
|
@@ -25377,6 +26141,47 @@ async function healthReportCommand(options) {
|
|
|
25377
26141
|
console.log(renderAscii2(report));
|
|
25378
26142
|
}
|
|
25379
26143
|
}
|
|
26144
|
+
async function sprintSummaryCommand(options) {
|
|
26145
|
+
const project = loadProject();
|
|
26146
|
+
const plugin = resolvePlugin(project.config.methodology);
|
|
26147
|
+
const pluginRegistrations = plugin?.documentTypeRegistrations ?? [];
|
|
26148
|
+
const allSkills = loadAllSkills(project.marvinDir);
|
|
26149
|
+
const allSkillIds = [...allSkills.keys()];
|
|
26150
|
+
const skillRegistrations = collectSkillRegistrations(allSkillIds, allSkills);
|
|
26151
|
+
const store = new DocumentStore(project.marvinDir, [...pluginRegistrations, ...skillRegistrations]);
|
|
26152
|
+
const data = collectSprintSummaryData(store, options.sprint);
|
|
26153
|
+
if (!data) {
|
|
26154
|
+
const msg = options.sprint ? `Sprint ${options.sprint} not found.` : "No active sprint found. Use --sprint <id> to specify one.";
|
|
26155
|
+
console.error(msg);
|
|
26156
|
+
process.exit(1);
|
|
26157
|
+
}
|
|
26158
|
+
const spinner = ora6({ text: "Generating AI sprint summary...", color: "cyan" }).start();
|
|
26159
|
+
try {
|
|
26160
|
+
const summary = await generateSprintSummary(data);
|
|
26161
|
+
spinner.stop();
|
|
26162
|
+
const header = `# Sprint Summary: ${data.sprint.id} \u2014 ${data.sprint.title}
|
|
26163
|
+
|
|
26164
|
+
`;
|
|
26165
|
+
console.log(header + summary);
|
|
26166
|
+
if (options.save) {
|
|
26167
|
+
const doc = store.create(
|
|
26168
|
+
"report",
|
|
26169
|
+
{
|
|
26170
|
+
title: `Sprint Summary: ${data.sprint.title}`,
|
|
26171
|
+
status: "final",
|
|
26172
|
+
tags: [`report-type:sprint-summary`, `sprint:${data.sprint.id}`]
|
|
26173
|
+
},
|
|
26174
|
+
summary
|
|
26175
|
+
);
|
|
26176
|
+
console.log(`
|
|
26177
|
+
Saved as ${doc.frontmatter.id}`);
|
|
26178
|
+
}
|
|
26179
|
+
} catch (err) {
|
|
26180
|
+
spinner.stop();
|
|
26181
|
+
console.error("Failed to generate sprint summary:", err);
|
|
26182
|
+
process.exit(1);
|
|
26183
|
+
}
|
|
26184
|
+
}
|
|
25380
26185
|
|
|
25381
26186
|
// src/cli/commands/web.ts
|
|
25382
26187
|
async function webCommand(options) {
|
|
@@ -25419,7 +26224,7 @@ function createProgram() {
|
|
|
25419
26224
|
const program2 = new Command();
|
|
25420
26225
|
program2.name("marvin").description(
|
|
25421
26226
|
"AI-powered product development assistant with Product Owner, Delivery Manager, and Technical Lead personas"
|
|
25422
|
-
).version("0.4.
|
|
26227
|
+
).version("0.4.8");
|
|
25423
26228
|
program2.command("init").description("Initialize a new Marvin project in the current directory").action(async () => {
|
|
25424
26229
|
await initCommand();
|
|
25425
26230
|
});
|
|
@@ -25502,6 +26307,9 @@ function createProgram() {
|
|
|
25502
26307
|
).action(async (options) => {
|
|
25503
26308
|
await healthReportCommand(options);
|
|
25504
26309
|
});
|
|
26310
|
+
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) => {
|
|
26311
|
+
await sprintSummaryCommand(options);
|
|
26312
|
+
});
|
|
25505
26313
|
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
26314
|
await webCommand(options);
|
|
25507
26315
|
});
|