mrvn-cli 0.4.4 → 0.4.5

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 CHANGED
@@ -15019,132 +15019,22 @@ function generateFeatureTags(features) {
15019
15019
  return features.map((id) => `feature:${id}`);
15020
15020
  }
15021
15021
 
15022
- // src/reports/gar/collector.ts
15023
- function collectGarMetrics(store) {
15024
- const allActions = store.list({ type: "action" });
15025
- const openActions = allActions.filter((d) => d.frontmatter.status === "open");
15026
- const doneActions = allActions.filter((d) => d.frontmatter.status === "done");
15027
- const allDocs = store.list();
15028
- const blockedItems = allDocs.filter(
15029
- (d) => d.frontmatter.tags?.includes("blocked")
15030
- );
15031
- const tagOverdueItems = allDocs.filter(
15032
- (d) => d.frontmatter.tags?.includes("overdue")
15033
- );
15034
- const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
15035
- const dateOverdueActions = openActions.filter((d) => {
15036
- const dueDate = d.frontmatter.dueDate;
15037
- return typeof dueDate === "string" && dueDate < today;
15038
- });
15039
- const overdueItems = [...tagOverdueItems, ...dateOverdueActions].filter(
15040
- (d, i, arr) => arr.findIndex((x) => x.frontmatter.id === d.frontmatter.id) === i
15041
- );
15042
- const openQuestions = store.list({ type: "question", status: "open" });
15043
- const riskItems = allDocs.filter(
15044
- (d) => d.frontmatter.tags?.includes("risk") && d.frontmatter.status !== "done" && d.frontmatter.status !== "closed"
15045
- );
15046
- const unownedActions = openActions.filter((d) => !d.frontmatter.owner);
15047
- const total = allActions.length;
15048
- const done = doneActions.length;
15049
- const completionPct = total > 0 ? Math.round(done / total * 100) : 100;
15050
- const scheduleItems = [
15051
- ...blockedItems,
15052
- ...overdueItems
15053
- ].filter(
15054
- (d, i, arr) => arr.findIndex((x) => x.frontmatter.id === d.frontmatter.id) === i
15055
- ).map((d) => ({ id: d.frontmatter.id, title: d.frontmatter.title }));
15056
- const qualityItems = [
15057
- ...riskItems,
15058
- ...openQuestions
15059
- ].filter(
15060
- (d, i, arr) => arr.findIndex((x) => x.frontmatter.id === d.frontmatter.id) === i
15061
- ).map((d) => ({ id: d.frontmatter.id, title: d.frontmatter.title }));
15062
- const resourceItems = unownedActions.map((d) => ({
15063
- id: d.frontmatter.id,
15064
- title: d.frontmatter.title
15065
- }));
15066
- return {
15067
- scope: {
15068
- total,
15069
- open: openActions.length,
15070
- done,
15071
- completionPct
15072
- },
15073
- schedule: {
15074
- blocked: blockedItems.length,
15075
- overdue: overdueItems.length,
15076
- items: scheduleItems
15077
- },
15078
- quality: {
15079
- risks: riskItems.length,
15080
- openQuestions: openQuestions.length,
15081
- items: qualityItems
15082
- },
15083
- resources: {
15084
- unowned: unownedActions.length,
15085
- items: resourceItems
15022
+ // src/plugins/builtin/tools/task-utils.ts
15023
+ function normalizeLinkedEpics(value) {
15024
+ if (value === void 0 || value === null) return [];
15025
+ if (typeof value === "string") {
15026
+ try {
15027
+ const parsed = JSON.parse(value);
15028
+ if (Array.isArray(parsed)) return parsed.filter((v) => typeof v === "string");
15029
+ } catch {
15086
15030
  }
15087
- };
15088
- }
15089
-
15090
- // src/reports/gar/evaluator.ts
15091
- function worstStatus(statuses) {
15092
- if (statuses.includes("red")) return "red";
15093
- if (statuses.includes("amber")) return "amber";
15094
- return "green";
15031
+ return [value];
15032
+ }
15033
+ if (Array.isArray(value)) return value.filter((v) => typeof v === "string");
15034
+ return [];
15095
15035
  }
15096
- function evaluateGar(projectName, metrics) {
15097
- const areas = [];
15098
- const scopePct = metrics.scope.completionPct;
15099
- const scopeStatus = scopePct >= 70 ? "green" : scopePct >= 40 ? "amber" : "red";
15100
- areas.push({
15101
- name: "Scope",
15102
- status: scopeStatus,
15103
- summary: `${scopePct}% complete (${metrics.scope.done}/${metrics.scope.total})`,
15104
- items: []
15105
- });
15106
- const scheduleCount = metrics.schedule.blocked + metrics.schedule.overdue;
15107
- const scheduleStatus = scheduleCount === 0 ? "green" : scheduleCount <= 2 ? "amber" : "red";
15108
- const scheduleParts = [];
15109
- if (metrics.schedule.blocked > 0)
15110
- scheduleParts.push(`${metrics.schedule.blocked} blocked`);
15111
- if (metrics.schedule.overdue > 0)
15112
- scheduleParts.push(`${metrics.schedule.overdue} overdue`);
15113
- areas.push({
15114
- name: "Schedule",
15115
- status: scheduleStatus,
15116
- summary: scheduleParts.length > 0 ? scheduleParts.join(", ") : "on track",
15117
- items: metrics.schedule.items
15118
- });
15119
- const qualityCount = metrics.quality.risks + metrics.quality.openQuestions;
15120
- const qualityStatus = qualityCount === 0 ? "green" : qualityCount <= 2 ? "amber" : "red";
15121
- const qualityParts = [];
15122
- if (metrics.quality.risks > 0)
15123
- qualityParts.push(`${metrics.quality.risks} risk(s)`);
15124
- if (metrics.quality.openQuestions > 0)
15125
- qualityParts.push(`${metrics.quality.openQuestions} open question(s)`);
15126
- areas.push({
15127
- name: "Quality",
15128
- status: qualityStatus,
15129
- summary: qualityParts.length > 0 ? qualityParts.join(", ") : "no issues",
15130
- items: metrics.quality.items
15131
- });
15132
- const resourceCount = metrics.resources.unowned;
15133
- const resourceStatus = resourceCount === 0 ? "green" : resourceCount <= 2 ? "amber" : "red";
15134
- areas.push({
15135
- name: "Resources",
15136
- status: resourceStatus,
15137
- summary: resourceCount > 0 ? `${resourceCount} unowned action(s)` : "all assigned",
15138
- items: metrics.resources.items
15139
- });
15140
- const overall = worstStatus(areas.map((a) => a.status));
15141
- return {
15142
- projectName,
15143
- generatedAt: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
15144
- overall,
15145
- areas,
15146
- metrics
15147
- };
15036
+ function generateEpicTags(epics) {
15037
+ return epics.map((id) => `epic:${id}`);
15148
15038
  }
15149
15039
 
15150
15040
  // src/reports/health/collector.ts
@@ -15286,6 +15176,134 @@ function collectHealthMetrics(store) {
15286
15176
  };
15287
15177
  }
15288
15178
 
15179
+ // src/reports/gar/collector.ts
15180
+ function collectGarMetrics(store) {
15181
+ const allActions = store.list({ type: "action" });
15182
+ const openActions = allActions.filter((d) => d.frontmatter.status === "open");
15183
+ const doneActions = allActions.filter((d) => d.frontmatter.status === "done");
15184
+ const allDocs = store.list();
15185
+ const blockedItems = allDocs.filter(
15186
+ (d) => d.frontmatter.tags?.includes("blocked")
15187
+ );
15188
+ const tagOverdueItems = allDocs.filter(
15189
+ (d) => d.frontmatter.tags?.includes("overdue")
15190
+ );
15191
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
15192
+ const dateOverdueActions = openActions.filter((d) => {
15193
+ const dueDate = d.frontmatter.dueDate;
15194
+ return typeof dueDate === "string" && dueDate < today;
15195
+ });
15196
+ const overdueItems = [...tagOverdueItems, ...dateOverdueActions].filter(
15197
+ (d, i, arr) => arr.findIndex((x) => x.frontmatter.id === d.frontmatter.id) === i
15198
+ );
15199
+ const openQuestions = store.list({ type: "question", status: "open" });
15200
+ const riskItems = allDocs.filter(
15201
+ (d) => d.frontmatter.tags?.includes("risk") && d.frontmatter.status !== "done" && d.frontmatter.status !== "closed"
15202
+ );
15203
+ const unownedActions = openActions.filter((d) => !d.frontmatter.owner);
15204
+ const total = allActions.length;
15205
+ const done = doneActions.length;
15206
+ const completionPct = total > 0 ? Math.round(done / total * 100) : 100;
15207
+ const scheduleItems = [
15208
+ ...blockedItems,
15209
+ ...overdueItems
15210
+ ].filter(
15211
+ (d, i, arr) => arr.findIndex((x) => x.frontmatter.id === d.frontmatter.id) === i
15212
+ ).map((d) => ({ id: d.frontmatter.id, title: d.frontmatter.title }));
15213
+ const qualityItems = [
15214
+ ...riskItems,
15215
+ ...openQuestions
15216
+ ].filter(
15217
+ (d, i, arr) => arr.findIndex((x) => x.frontmatter.id === d.frontmatter.id) === i
15218
+ ).map((d) => ({ id: d.frontmatter.id, title: d.frontmatter.title }));
15219
+ const resourceItems = unownedActions.map((d) => ({
15220
+ id: d.frontmatter.id,
15221
+ title: d.frontmatter.title
15222
+ }));
15223
+ return {
15224
+ scope: {
15225
+ total,
15226
+ open: openActions.length,
15227
+ done,
15228
+ completionPct
15229
+ },
15230
+ schedule: {
15231
+ blocked: blockedItems.length,
15232
+ overdue: overdueItems.length,
15233
+ items: scheduleItems
15234
+ },
15235
+ quality: {
15236
+ risks: riskItems.length,
15237
+ openQuestions: openQuestions.length,
15238
+ items: qualityItems
15239
+ },
15240
+ resources: {
15241
+ unowned: unownedActions.length,
15242
+ items: resourceItems
15243
+ }
15244
+ };
15245
+ }
15246
+
15247
+ // src/reports/gar/evaluator.ts
15248
+ function worstStatus(statuses) {
15249
+ if (statuses.includes("red")) return "red";
15250
+ if (statuses.includes("amber")) return "amber";
15251
+ return "green";
15252
+ }
15253
+ function evaluateGar(projectName, metrics) {
15254
+ const areas = [];
15255
+ const scopePct = metrics.scope.completionPct;
15256
+ const scopeStatus = scopePct >= 70 ? "green" : scopePct >= 40 ? "amber" : "red";
15257
+ areas.push({
15258
+ name: "Scope",
15259
+ status: scopeStatus,
15260
+ summary: `${scopePct}% complete (${metrics.scope.done}/${metrics.scope.total})`,
15261
+ items: []
15262
+ });
15263
+ const scheduleCount = metrics.schedule.blocked + metrics.schedule.overdue;
15264
+ const scheduleStatus = scheduleCount === 0 ? "green" : scheduleCount <= 2 ? "amber" : "red";
15265
+ const scheduleParts = [];
15266
+ if (metrics.schedule.blocked > 0)
15267
+ scheduleParts.push(`${metrics.schedule.blocked} blocked`);
15268
+ if (metrics.schedule.overdue > 0)
15269
+ scheduleParts.push(`${metrics.schedule.overdue} overdue`);
15270
+ areas.push({
15271
+ name: "Schedule",
15272
+ status: scheduleStatus,
15273
+ summary: scheduleParts.length > 0 ? scheduleParts.join(", ") : "on track",
15274
+ items: metrics.schedule.items
15275
+ });
15276
+ const qualityCount = metrics.quality.risks + metrics.quality.openQuestions;
15277
+ const qualityStatus = qualityCount === 0 ? "green" : qualityCount <= 2 ? "amber" : "red";
15278
+ const qualityParts = [];
15279
+ if (metrics.quality.risks > 0)
15280
+ qualityParts.push(`${metrics.quality.risks} risk(s)`);
15281
+ if (metrics.quality.openQuestions > 0)
15282
+ qualityParts.push(`${metrics.quality.openQuestions} open question(s)`);
15283
+ areas.push({
15284
+ name: "Quality",
15285
+ status: qualityStatus,
15286
+ summary: qualityParts.length > 0 ? qualityParts.join(", ") : "no issues",
15287
+ items: metrics.quality.items
15288
+ });
15289
+ const resourceCount = metrics.resources.unowned;
15290
+ const resourceStatus = resourceCount === 0 ? "green" : resourceCount <= 2 ? "amber" : "red";
15291
+ areas.push({
15292
+ name: "Resources",
15293
+ status: resourceStatus,
15294
+ summary: resourceCount > 0 ? `${resourceCount} unowned action(s)` : "all assigned",
15295
+ items: metrics.resources.items
15296
+ });
15297
+ const overall = worstStatus(areas.map((a) => a.status));
15298
+ return {
15299
+ projectName,
15300
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
15301
+ overall,
15302
+ areas,
15303
+ metrics
15304
+ };
15305
+ }
15306
+
15289
15307
  // src/reports/health/evaluator.ts
15290
15308
  function worstStatus2(statuses) {
15291
15309
  if (statuses.includes("red")) return "red";
@@ -15505,6 +15523,204 @@ function getDiagramData(store) {
15505
15523
  }
15506
15524
  return { sprints, epics, features, statusCounts };
15507
15525
  }
15526
+ function computeUrgency(dueDateStr, todayStr) {
15527
+ const due = new Date(dueDateStr).getTime();
15528
+ const today = new Date(todayStr).getTime();
15529
+ const diffDays = Math.floor((due - today) / 864e5);
15530
+ if (diffDays < 0) return "overdue";
15531
+ if (diffDays <= 3) return "due-3d";
15532
+ if (diffDays <= 7) return "due-7d";
15533
+ if (diffDays <= 14) return "upcoming";
15534
+ return "later";
15535
+ }
15536
+ var DONE_STATUSES = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
15537
+ function getUpcomingData(store) {
15538
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
15539
+ const allDocs = store.list();
15540
+ const docById = /* @__PURE__ */ new Map();
15541
+ for (const doc of allDocs) {
15542
+ docById.set(doc.frontmatter.id, doc);
15543
+ }
15544
+ const actions = allDocs.filter(
15545
+ (d) => d.frontmatter.type === "action" && !DONE_STATUSES.has(d.frontmatter.status)
15546
+ );
15547
+ const actionsWithDue = actions.filter((d) => d.frontmatter.dueDate);
15548
+ const sprints = allDocs.filter((d) => d.frontmatter.type === "sprint");
15549
+ const epics = allDocs.filter((d) => d.frontmatter.type === "epic");
15550
+ const tasks = allDocs.filter((d) => d.frontmatter.type === "task");
15551
+ const epicToTasks = /* @__PURE__ */ new Map();
15552
+ for (const task of tasks) {
15553
+ const tags = task.frontmatter.tags ?? [];
15554
+ for (const tag of tags) {
15555
+ if (tag.startsWith("epic:")) {
15556
+ const epicId = tag.slice(5);
15557
+ if (!epicToTasks.has(epicId)) epicToTasks.set(epicId, []);
15558
+ epicToTasks.get(epicId).push(task);
15559
+ }
15560
+ }
15561
+ }
15562
+ function getSprintTasks(sprintDoc) {
15563
+ const linkedEpics = normalizeLinkedEpics(sprintDoc.frontmatter.linkedEpics);
15564
+ const result = [];
15565
+ for (const epicId of linkedEpics) {
15566
+ const epicTasks = epicToTasks.get(epicId) ?? [];
15567
+ result.push(...epicTasks);
15568
+ }
15569
+ return result;
15570
+ }
15571
+ function countRelatedTasks(actionDoc) {
15572
+ const actionTags = actionDoc.frontmatter.tags ?? [];
15573
+ const relatedTaskIds = /* @__PURE__ */ new Set();
15574
+ for (const tag of actionTags) {
15575
+ if (tag.startsWith("sprint:")) {
15576
+ const sprintId = tag.slice(7);
15577
+ const sprint = docById.get(sprintId);
15578
+ if (sprint) {
15579
+ const sprintTaskDocs = getSprintTasks(sprint);
15580
+ for (const t of sprintTaskDocs) relatedTaskIds.add(t.frontmatter.id);
15581
+ }
15582
+ }
15583
+ }
15584
+ return relatedTaskIds.size;
15585
+ }
15586
+ const dueSoonActions = actionsWithDue.map((d) => ({
15587
+ id: d.frontmatter.id,
15588
+ title: d.frontmatter.title,
15589
+ status: d.frontmatter.status,
15590
+ owner: d.frontmatter.owner,
15591
+ dueDate: d.frontmatter.dueDate,
15592
+ urgency: computeUrgency(d.frontmatter.dueDate, today),
15593
+ relatedTaskCount: countRelatedTasks(d)
15594
+ })).sort((a, b) => a.dueDate.localeCompare(b.dueDate));
15595
+ const todayMs = new Date(today).getTime();
15596
+ const fourteenDaysMs = 14 * 864e5;
15597
+ const nearSprints = sprints.filter((s) => {
15598
+ const endDate = s.frontmatter.endDate;
15599
+ if (!endDate) return false;
15600
+ const endMs = new Date(endDate).getTime();
15601
+ const diff = endMs - todayMs;
15602
+ return diff >= 0 && diff <= fourteenDaysMs;
15603
+ });
15604
+ const taskSprintMap = /* @__PURE__ */ new Map();
15605
+ for (const sprint of nearSprints) {
15606
+ const sprintEnd = sprint.frontmatter.endDate;
15607
+ const sprintTaskDocs = getSprintTasks(sprint);
15608
+ for (const task of sprintTaskDocs) {
15609
+ if (DONE_STATUSES.has(task.frontmatter.status)) continue;
15610
+ const existing = taskSprintMap.get(task.frontmatter.id);
15611
+ if (!existing || sprintEnd < existing.sprintEnd) {
15612
+ taskSprintMap.set(task.frontmatter.id, { task, sprint, sprintEnd });
15613
+ }
15614
+ }
15615
+ }
15616
+ const dueSoonSprintTasks = [...taskSprintMap.values()].map(({ task, sprint, sprintEnd }) => ({
15617
+ id: task.frontmatter.id,
15618
+ title: task.frontmatter.title,
15619
+ status: task.frontmatter.status,
15620
+ sprintId: sprint.frontmatter.id,
15621
+ sprintTitle: sprint.frontmatter.title,
15622
+ sprintEndDate: sprintEnd,
15623
+ urgency: computeUrgency(sprintEnd, today)
15624
+ })).sort((a, b) => a.sprintEndDate.localeCompare(b.sprintEndDate));
15625
+ const openItems = allDocs.filter(
15626
+ (d) => ["action", "question", "task"].includes(d.frontmatter.type) && !DONE_STATUSES.has(d.frontmatter.status)
15627
+ );
15628
+ const fourteenDaysAgo = new Date(todayMs - fourteenDaysMs).toISOString().slice(0, 10);
15629
+ const recentMeetings = allDocs.filter(
15630
+ (d) => d.frontmatter.type === "meeting" && (d.frontmatter.updated ?? d.frontmatter.created) >= fourteenDaysAgo
15631
+ );
15632
+ const crossRefCounts = /* @__PURE__ */ new Map();
15633
+ for (const doc of allDocs) {
15634
+ const content = doc.content ?? "";
15635
+ for (const item of openItems) {
15636
+ if (doc.frontmatter.id === item.frontmatter.id) continue;
15637
+ if (content.includes(item.frontmatter.id)) {
15638
+ crossRefCounts.set(
15639
+ item.frontmatter.id,
15640
+ (crossRefCounts.get(item.frontmatter.id) ?? 0) + 1
15641
+ );
15642
+ }
15643
+ }
15644
+ }
15645
+ const activeSprints = sprints.filter((s) => {
15646
+ const status = s.frontmatter.status;
15647
+ if (status === "active") return true;
15648
+ const startDate = s.frontmatter.startDate;
15649
+ if (!startDate) return false;
15650
+ const startMs = new Date(startDate).getTime();
15651
+ const diff = startMs - todayMs;
15652
+ return diff >= 0 && diff <= fourteenDaysMs;
15653
+ });
15654
+ const activeSprintIds = new Set(activeSprints.map((s) => s.frontmatter.id));
15655
+ const activeEpicIds = /* @__PURE__ */ new Set();
15656
+ for (const s of activeSprints) {
15657
+ for (const epicId of normalizeLinkedEpics(s.frontmatter.linkedEpics)) {
15658
+ activeEpicIds.add(epicId);
15659
+ }
15660
+ }
15661
+ const trending = openItems.map((doc) => {
15662
+ const signals = [];
15663
+ let score = 0;
15664
+ const updated = doc.frontmatter.updated ?? doc.frontmatter.created;
15665
+ const ageDays = daysBetween(updated, today);
15666
+ const recencyPts = Math.max(0, Math.round(20 * (1 - ageDays / 30)));
15667
+ if (recencyPts > 0) {
15668
+ signals.push({ factor: "recency", points: recencyPts });
15669
+ score += recencyPts;
15670
+ }
15671
+ const tags = doc.frontmatter.tags ?? [];
15672
+ const linkedToActiveSprint = tags.some(
15673
+ (t) => t.startsWith("sprint:") && activeSprintIds.has(t.slice(7))
15674
+ );
15675
+ const linkedToActiveEpic = tags.some(
15676
+ (t) => t.startsWith("epic:") && activeEpicIds.has(t.slice(5))
15677
+ );
15678
+ if (linkedToActiveSprint) {
15679
+ signals.push({ factor: "sprint proximity", points: 25 });
15680
+ score += 25;
15681
+ } else if (linkedToActiveEpic) {
15682
+ signals.push({ factor: "sprint proximity", points: 15 });
15683
+ score += 15;
15684
+ }
15685
+ const mentionCount = recentMeetings.filter(
15686
+ (m) => (m.content ?? "").includes(doc.frontmatter.id)
15687
+ ).length;
15688
+ if (mentionCount > 0) {
15689
+ const meetingPts = Math.min(15, mentionCount * 5);
15690
+ signals.push({ factor: "meeting mentions", points: meetingPts });
15691
+ score += meetingPts;
15692
+ }
15693
+ const priority = doc.frontmatter.priority?.toLowerCase();
15694
+ const priorityPts = priority === "critical" ? 15 : priority === "high" ? 10 : priority === "medium" ? 3 : 0;
15695
+ if (priorityPts > 0) {
15696
+ signals.push({ factor: "priority", points: priorityPts });
15697
+ score += priorityPts;
15698
+ }
15699
+ if (["action", "question"].includes(doc.frontmatter.type)) {
15700
+ const createdDays = daysBetween(doc.frontmatter.created, today);
15701
+ if (createdDays >= 14) {
15702
+ const agingPts = Math.min(10, Math.floor((createdDays - 14) / 7) * 3 + 5);
15703
+ signals.push({ factor: "aging", points: agingPts });
15704
+ score += agingPts;
15705
+ }
15706
+ }
15707
+ const refs = crossRefCounts.get(doc.frontmatter.id) ?? 0;
15708
+ if (refs > 0) {
15709
+ const crossRefPts = Math.min(15, refs * 5);
15710
+ signals.push({ factor: "cross-references", points: crossRefPts });
15711
+ score += crossRefPts;
15712
+ }
15713
+ return {
15714
+ id: doc.frontmatter.id,
15715
+ title: doc.frontmatter.title,
15716
+ type: doc.frontmatter.type,
15717
+ status: doc.frontmatter.status,
15718
+ score,
15719
+ signals
15720
+ };
15721
+ }).filter((item) => item.score > 0).sort((a, b) => b.score - a.score).slice(0, 15);
15722
+ return { dueSoonActions, dueSoonSprintTasks, trending };
15723
+ }
15508
15724
 
15509
15725
  // src/web/templates/layout.ts
15510
15726
  function escapeHtml(str) {
@@ -15628,6 +15844,7 @@ function inline(text) {
15628
15844
  function layout(opts, body) {
15629
15845
  const topItems = [
15630
15846
  { href: "/", label: "Overview" },
15847
+ { href: "/upcoming", label: "Upcoming" },
15631
15848
  { href: "/timeline", label: "Timeline" },
15632
15849
  { href: "/board", label: "Board" },
15633
15850
  { href: "/gar", label: "GAR Report" },
@@ -16514,6 +16731,56 @@ tr:hover td {
16514
16731
  fill: var(--bg) !important;
16515
16732
  font-weight: 600;
16516
16733
  }
16734
+
16735
+ /* Urgency row indicators */
16736
+ .urgency-row-overdue { border-left: 3px solid var(--red); }
16737
+ .urgency-row-due-3d { border-left: 3px solid var(--amber); }
16738
+ .urgency-row-due-7d { border-left: 3px solid #e2a308; }
16739
+
16740
+ /* Urgency badge pills */
16741
+ .urgency-badge-overdue { background: rgba(248, 113, 113, 0.15); color: var(--red); }
16742
+ .urgency-badge-due-3d { background: rgba(251, 191, 36, 0.15); color: var(--amber); }
16743
+ .urgency-badge-due-7d { background: rgba(226, 163, 8, 0.15); color: #e2a308; }
16744
+ .urgency-badge-upcoming { background: rgba(108, 140, 255, 0.15); color: var(--accent); }
16745
+ .urgency-badge-later { background: rgba(139, 143, 164, 0.1); color: var(--text-dim); }
16746
+
16747
+ /* Trending */
16748
+ .trending-rank {
16749
+ display: inline-flex;
16750
+ align-items: center;
16751
+ justify-content: center;
16752
+ width: 24px;
16753
+ height: 24px;
16754
+ border-radius: 50%;
16755
+ background: var(--bg-hover);
16756
+ font-size: 0.75rem;
16757
+ font-weight: 600;
16758
+ color: var(--text-dim);
16759
+ }
16760
+
16761
+ .trending-score {
16762
+ display: inline-block;
16763
+ padding: 0.15rem 0.6rem;
16764
+ border-radius: 999px;
16765
+ font-size: 0.7rem;
16766
+ font-weight: 700;
16767
+ background: rgba(108, 140, 255, 0.15);
16768
+ color: var(--accent);
16769
+ }
16770
+
16771
+ .signal-tag {
16772
+ display: inline-block;
16773
+ padding: 0.1rem 0.45rem;
16774
+ border-radius: 4px;
16775
+ font-size: 0.65rem;
16776
+ background: var(--bg-hover);
16777
+ color: var(--text-dim);
16778
+ margin-right: 0.25rem;
16779
+ margin-bottom: 0.15rem;
16780
+ white-space: nowrap;
16781
+ }
16782
+
16783
+ .text-dim { color: var(--text-dim); }
16517
16784
  `;
16518
16785
  }
16519
16786
 
@@ -16686,13 +16953,14 @@ function buildArtifactFlowchart(data) {
16686
16953
  var svg = document.getElementById('flow-lines');
16687
16954
  if (!container || !svg) return;
16688
16955
 
16689
- // Build adjacency map (bidirectional) for traversal
16690
- var adj = {};
16956
+ // Build directed adjacency maps for traversal
16957
+ var fwd = {}; // from \u2192 [to] (Feature\u2192Epic, Epic\u2192Sprint)
16958
+ var bwd = {}; // to \u2192 [from] (Sprint\u2192Epic, Epic\u2192Feature)
16691
16959
  edges.forEach(function(e) {
16692
- if (!adj[e.from]) adj[e.from] = [];
16693
- if (!adj[e.to]) adj[e.to] = [];
16694
- adj[e.from].push(e.to);
16695
- adj[e.to].push(e.from);
16960
+ if (!fwd[e.from]) fwd[e.from] = [];
16961
+ if (!bwd[e.to]) bwd[e.to] = [];
16962
+ fwd[e.from].push(e.to);
16963
+ bwd[e.to].push(e.from);
16696
16964
  });
16697
16965
 
16698
16966
  function drawLines() {
@@ -16725,14 +16993,28 @@ function buildArtifactFlowchart(data) {
16725
16993
  });
16726
16994
  }
16727
16995
 
16728
- // Find all nodes reachable from a starting node
16996
+ // Find directly related nodes via directed traversal
16997
+ // Follows forward edges (Feature\u2192Epic\u2192Sprint) and backward edges
16998
+ // (Sprint\u2192Epic\u2192Feature) separately to avoid sideways expansion
16729
16999
  function findConnected(startId) {
16730
17000
  var visited = {};
16731
- var queue = [startId];
16732
17001
  visited[startId] = true;
17002
+ // Traverse forward (from\u2192to direction)
17003
+ var queue = [startId];
16733
17004
  while (queue.length) {
16734
17005
  var id = queue.shift();
16735
- (adj[id] || []).forEach(function(neighbor) {
17006
+ (fwd[id] || []).forEach(function(neighbor) {
17007
+ if (!visited[neighbor]) {
17008
+ visited[neighbor] = true;
17009
+ queue.push(neighbor);
17010
+ }
17011
+ });
17012
+ }
17013
+ // Traverse backward (to\u2192from direction)
17014
+ queue = [startId];
17015
+ while (queue.length) {
17016
+ var id = queue.shift();
17017
+ (bwd[id] || []).forEach(function(neighbor) {
16736
17018
  if (!visited[neighbor]) {
16737
17019
  visited[neighbor] = true;
16738
17020
  queue.push(neighbor);
@@ -17174,6 +17456,131 @@ function timelinePage(diagrams) {
17174
17456
  `;
17175
17457
  }
17176
17458
 
17459
+ // src/web/templates/pages/upcoming.ts
17460
+ function urgencyBadge(tier) {
17461
+ const labels = {
17462
+ overdue: "Overdue",
17463
+ "due-3d": "Due in 3d",
17464
+ "due-7d": "Due in 7d",
17465
+ upcoming: "Upcoming",
17466
+ later: "Later"
17467
+ };
17468
+ return `<span class="badge urgency-badge-${tier}">${labels[tier]}</span>`;
17469
+ }
17470
+ function urgencyRowClass(tier) {
17471
+ if (tier === "overdue") return " urgency-row-overdue";
17472
+ if (tier === "due-3d") return " urgency-row-due-3d";
17473
+ if (tier === "due-7d") return " urgency-row-due-7d";
17474
+ return "";
17475
+ }
17476
+ function upcomingPage(data) {
17477
+ const hasActions = data.dueSoonActions.length > 0;
17478
+ const hasSprintTasks = data.dueSoonSprintTasks.length > 0;
17479
+ const hasTrending = data.trending.length > 0;
17480
+ const actionsTable = hasActions ? `
17481
+ <h3 class="section-title">Due Soon \u2014 Actions</h3>
17482
+ <div class="table-wrap">
17483
+ <table>
17484
+ <thead>
17485
+ <tr>
17486
+ <th>ID</th>
17487
+ <th>Title</th>
17488
+ <th>Status</th>
17489
+ <th>Owner</th>
17490
+ <th>Due Date</th>
17491
+ <th>Urgency</th>
17492
+ <th>Tasks</th>
17493
+ </tr>
17494
+ </thead>
17495
+ <tbody>
17496
+ ${data.dueSoonActions.map(
17497
+ (a) => `
17498
+ <tr class="${urgencyRowClass(a.urgency)}">
17499
+ <td><a href="/docs/action/${escapeHtml(a.id)}">${escapeHtml(a.id)}</a></td>
17500
+ <td>${escapeHtml(a.title)}</td>
17501
+ <td>${statusBadge(a.status)}</td>
17502
+ <td>${a.owner ? escapeHtml(a.owner) : '<span class="text-dim">\u2014</span>'}</td>
17503
+ <td>${formatDate(a.dueDate)}</td>
17504
+ <td>${urgencyBadge(a.urgency)}</td>
17505
+ <td>${a.relatedTaskCount > 0 ? a.relatedTaskCount : "\u2014"}</td>
17506
+ </tr>`
17507
+ ).join("")}
17508
+ </tbody>
17509
+ </table>
17510
+ </div>` : "";
17511
+ const sprintTasksTable = hasSprintTasks ? `
17512
+ <h3 class="section-title">Due Soon \u2014 Sprint Tasks</h3>
17513
+ <div class="table-wrap">
17514
+ <table>
17515
+ <thead>
17516
+ <tr>
17517
+ <th>ID</th>
17518
+ <th>Title</th>
17519
+ <th>Status</th>
17520
+ <th>Sprint</th>
17521
+ <th>Sprint Ends</th>
17522
+ <th>Urgency</th>
17523
+ </tr>
17524
+ </thead>
17525
+ <tbody>
17526
+ ${data.dueSoonSprintTasks.map(
17527
+ (t) => `
17528
+ <tr class="${urgencyRowClass(t.urgency)}">
17529
+ <td><a href="/docs/task/${escapeHtml(t.id)}">${escapeHtml(t.id)}</a></td>
17530
+ <td>${escapeHtml(t.title)}</td>
17531
+ <td>${statusBadge(t.status)}</td>
17532
+ <td><a href="/docs/sprint/${escapeHtml(t.sprintId)}">${escapeHtml(t.sprintId)}</a></td>
17533
+ <td>${formatDate(t.sprintEndDate)}</td>
17534
+ <td>${urgencyBadge(t.urgency)}</td>
17535
+ </tr>`
17536
+ ).join("")}
17537
+ </tbody>
17538
+ </table>
17539
+ </div>` : "";
17540
+ const trendingTable = hasTrending ? `
17541
+ <h3 class="section-title">Trending</h3>
17542
+ <div class="table-wrap">
17543
+ <table>
17544
+ <thead>
17545
+ <tr>
17546
+ <th>#</th>
17547
+ <th>ID</th>
17548
+ <th>Title</th>
17549
+ <th>Type</th>
17550
+ <th>Status</th>
17551
+ <th>Score</th>
17552
+ <th>Signals</th>
17553
+ </tr>
17554
+ </thead>
17555
+ <tbody>
17556
+ ${data.trending.map(
17557
+ (t, i) => `
17558
+ <tr>
17559
+ <td><span class="trending-rank">${i + 1}</span></td>
17560
+ <td><a href="/docs/${escapeHtml(t.type)}/${escapeHtml(t.id)}">${escapeHtml(t.id)}</a></td>
17561
+ <td>${escapeHtml(t.title)}</td>
17562
+ <td>${escapeHtml(typeLabel(t.type))}</td>
17563
+ <td>${statusBadge(t.status)}</td>
17564
+ <td><span class="trending-score">${t.score}</span></td>
17565
+ <td>${t.signals.map((s) => `<span class="signal-tag">${escapeHtml(s.factor)} +${s.points}</span>`).join(" ")}</td>
17566
+ </tr>`
17567
+ ).join("")}
17568
+ </tbody>
17569
+ </table>
17570
+ </div>` : "";
17571
+ const emptyState = !hasActions && !hasSprintTasks && !hasTrending ? '<div class="empty"><p>No upcoming items or trending activity found.</p></div>' : "";
17572
+ return `
17573
+ <div class="page-header">
17574
+ <h2>Upcoming</h2>
17575
+ <div class="subtitle">Time-sensitive items and trending activity</div>
17576
+ </div>
17577
+ ${actionsTable}
17578
+ ${sprintTasksTable}
17579
+ ${trendingTable}
17580
+ ${emptyState}
17581
+ `;
17582
+ }
17583
+
17177
17584
  // src/web/router.ts
17178
17585
  function handleRequest(req, res, store, projectName, navGroups) {
17179
17586
  const parsed = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
@@ -17214,6 +17621,12 @@ function handleRequest(req, res, store, projectName, navGroups) {
17214
17621
  respond(res, layout({ title: "Health Check", activePath: "/health", projectName, navGroups }, body));
17215
17622
  return;
17216
17623
  }
17624
+ if (pathname === "/upcoming") {
17625
+ const data = getUpcomingData(store);
17626
+ const body = upcomingPage(data);
17627
+ respond(res, layout({ title: "Upcoming", activePath: "/upcoming", projectName, navGroups }, body));
17628
+ return;
17629
+ }
17217
17630
  const boardMatch = pathname.match(/^\/board(?:\/([^/]+))?$/);
17218
17631
  if (boardMatch) {
17219
17632
  const type = boardMatch[1];
@@ -18586,26 +18999,6 @@ function createSprintPlanningTools(store) {
18586
18999
 
18587
19000
  // src/plugins/builtin/tools/tasks.ts
18588
19001
  import { tool as tool14 } from "@anthropic-ai/claude-agent-sdk";
18589
-
18590
- // src/plugins/builtin/tools/task-utils.ts
18591
- function normalizeLinkedEpics(value) {
18592
- if (value === void 0 || value === null) return [];
18593
- if (typeof value === "string") {
18594
- try {
18595
- const parsed = JSON.parse(value);
18596
- if (Array.isArray(parsed)) return parsed.filter((v) => typeof v === "string");
18597
- } catch {
18598
- }
18599
- return [value];
18600
- }
18601
- if (Array.isArray(value)) return value.filter((v) => typeof v === "string");
18602
- return [];
18603
- }
18604
- function generateEpicTags(epics) {
18605
- return epics.map((id) => `epic:${id}`);
18606
- }
18607
-
18608
- // src/plugins/builtin/tools/tasks.ts
18609
19002
  var linkedEpicArray = external_exports.preprocess(
18610
19003
  (val) => {
18611
19004
  if (typeof val === "string") {
@@ -20024,8 +20417,9 @@ function findByJiraKey(store, jiraKey) {
20024
20417
  const docs = store.list({ type: JIRA_TYPE });
20025
20418
  return docs.find((d) => d.frontmatter.jiraKey === jiraKey);
20026
20419
  }
20027
- function createJiraTools(store) {
20420
+ function createJiraTools(store, projectConfig) {
20028
20421
  const jiraUserConfig = loadUserConfig().jira;
20422
+ const defaultProjectKey = projectConfig?.jira?.projectKey;
20029
20423
  return [
20030
20424
  // --- Local read tools ---
20031
20425
  tool20(
@@ -20189,10 +20583,22 @@ function createJiraTools(store) {
20189
20583
  "Create a Jira issue from any Marvin artifact (D/A/Q/F/E) and create a tracking JI-xxx document",
20190
20584
  {
20191
20585
  artifactId: external_exports.string().describe("Marvin artifact ID (e.g. 'D-001', 'F-003', 'E-002')"),
20192
- projectKey: external_exports.string().describe("Jira project key (e.g. 'PROJ')"),
20586
+ projectKey: external_exports.string().optional().describe("Jira project key (e.g. 'PROJ'). Falls back to jira.projectKey from .marvin/config.yaml if not provided."),
20193
20587
  issueType: external_exports.enum(["Story", "Task", "Bug", "Epic"]).optional().describe("Jira issue type (default: 'Task')")
20194
20588
  },
20195
20589
  async (args) => {
20590
+ const resolvedProjectKey = args.projectKey ?? defaultProjectKey;
20591
+ if (!resolvedProjectKey) {
20592
+ return {
20593
+ content: [
20594
+ {
20595
+ type: "text",
20596
+ text: "No projectKey provided and no default configured. Either pass projectKey or set jira.projectKey in .marvin/config.yaml."
20597
+ }
20598
+ ],
20599
+ isError: true
20600
+ };
20601
+ }
20196
20602
  const jira = createJiraClient(jiraUserConfig);
20197
20603
  if (!jira) return jiraNotConfiguredError();
20198
20604
  const artifact = store.get(args.artifactId);
@@ -20212,7 +20618,7 @@ function createJiraTools(store) {
20212
20618
  `Status: ${artifact.frontmatter.status}`
20213
20619
  ].join("\n");
20214
20620
  const jiraResult = await jira.client.createIssue({
20215
- project: { key: args.projectKey },
20621
+ project: { key: resolvedProjectKey },
20216
20622
  summary: artifact.frontmatter.title,
20217
20623
  description,
20218
20624
  issuetype: { name: args.issueType ?? "Task" }
@@ -20353,14 +20759,14 @@ var jiraSkill = {
20353
20759
  documentTypeRegistrations: [
20354
20760
  { type: "jira-issue", dirName: "jira-issues", idPrefix: "JI" }
20355
20761
  ],
20356
- tools: (store) => createJiraTools(store),
20762
+ tools: (store, projectConfig) => createJiraTools(store, projectConfig),
20357
20763
  promptFragments: {
20358
20764
  "product-owner": `You have the **Jira Integration** skill. You can pull issues from Jira and push Marvin artifacts to Jira.
20359
20765
 
20360
20766
  **Available tools:**
20361
20767
  - \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues
20362
20768
  - \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import issues from Jira by key or JQL query
20363
- - \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, feature, etc.)
20769
+ - \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, feature, etc.). The \`projectKey\` parameter is optional when a default is configured in \`.marvin/config.yaml\` under \`jira.projectKey\`.
20364
20770
  - \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
20365
20771
  - \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx
20366
20772
 
@@ -20374,7 +20780,7 @@ var jiraSkill = {
20374
20780
  **Available tools:**
20375
20781
  - \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues
20376
20782
  - \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import issues from Jira by key or JQL query
20377
- - \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, action, epic, task, etc.)
20783
+ - \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, action, epic, task, etc.). The \`projectKey\` parameter is optional when a default is configured in \`.marvin/config.yaml\` under \`jira.projectKey\`.
20378
20784
  - \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
20379
20785
  - \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx
20380
20786
 
@@ -20388,7 +20794,7 @@ var jiraSkill = {
20388
20794
  **Available tools:**
20389
20795
  - \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues
20390
20796
  - \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import issues from Jira by key or JQL query
20391
- - \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, action, etc.)
20797
+ - \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, action, etc.). The \`projectKey\` parameter is optional when a default is configured in \`.marvin/config.yaml\` under \`jira.projectKey\`.
20392
20798
  - \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
20393
20799
  - \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx
20394
20800
 
@@ -20966,12 +21372,12 @@ function collectSkillRegistrations(skillIds, allSkills) {
20966
21372
  }
20967
21373
  return registrations;
20968
21374
  }
20969
- function getSkillTools(skillIds, allSkills, store) {
21375
+ function getSkillTools(skillIds, allSkills, store, projectConfig) {
20970
21376
  const tools = [];
20971
21377
  for (const id of skillIds) {
20972
21378
  const skill = allSkills.get(id);
20973
21379
  if (skill?.tools) {
20974
- tools.push(...skill.tools(store));
21380
+ tools.push(...skill.tools(store, projectConfig));
20975
21381
  }
20976
21382
  }
20977
21383
  return tools;
@@ -21211,6 +21617,7 @@ function createWebTools(store, projectName, navGroups) {
21211
21617
  const base = `http://localhost:${runningServer.port}`;
21212
21618
  const urls = {
21213
21619
  overview: base,
21620
+ upcoming: `${base}/upcoming`,
21214
21621
  gar: `${base}/gar`,
21215
21622
  board: `${base}/board`
21216
21623
  };
@@ -21284,6 +21691,18 @@ function createWebTools(store, projectName, navGroups) {
21284
21691
  };
21285
21692
  },
21286
21693
  { annotations: { readOnlyHint: true } }
21694
+ ),
21695
+ tool22(
21696
+ "get_dashboard_upcoming",
21697
+ "Get upcoming data: due-soon actions and sprint tasks, plus trending items scored by relevance signals. Works without the web server running.",
21698
+ {},
21699
+ async () => {
21700
+ const data = getUpcomingData(store);
21701
+ return {
21702
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
21703
+ };
21704
+ },
21705
+ { annotations: { readOnlyHint: true } }
21287
21706
  )
21288
21707
  ];
21289
21708
  }
@@ -21544,7 +21963,7 @@ async function startSession(options) {
21544
21963
  const manifest = hasSourcesDir ? new SourceManifestManager(marvinDir) : void 0;
21545
21964
  const pluginTools = plugin ? getPluginTools(plugin, store, marvinDir) : [];
21546
21965
  const pluginPromptFragment = plugin ? getPluginPromptFragment(plugin, persona.id) : void 0;
21547
- const codeSkillTools = getSkillTools(skillIds, allSkills, store);
21966
+ const codeSkillTools = getSkillTools(skillIds, allSkills, store, config2.project);
21548
21967
  const skillAgents = getSkillAgentDefinitions(skillIds, allSkills);
21549
21968
  const skillPromptFragment = getSkillPromptFragment(skillIds, allSkills, persona.id);
21550
21969
  const allSkillIds = [...allSkills.keys()];
@@ -21656,6 +22075,7 @@ Marvin \u2014 ${persona.name}
21656
22075
  "mcp__marvin-governance__get_dashboard_overview",
21657
22076
  "mcp__marvin-governance__get_dashboard_gar",
21658
22077
  "mcp__marvin-governance__get_dashboard_board",
22078
+ "mcp__marvin-governance__get_dashboard_upcoming",
21659
22079
  ...pluginTools.map((t) => `mcp__marvin-governance__${t.name}`),
21660
22080
  ...codeSkillTools.map((t) => `mcp__marvin-governance__${t.name}`)
21661
22081
  ]
@@ -22055,7 +22475,7 @@ function collectTools(marvinDir) {
22055
22475
  const sessionStore = new SessionStore(marvinDir);
22056
22476
  const allSkills = loadAllSkills(marvinDir);
22057
22477
  const allSkillIds = [...allSkills.keys()];
22058
- const codeSkillTools = getSkillTools(allSkillIds, allSkills, store);
22478
+ const codeSkillTools = getSkillTools(allSkillIds, allSkills, store, config2);
22059
22479
  const skillsWithActions = allSkillIds.map((id) => allSkills.get(id)).filter((s) => s.actions && s.actions.length > 0);
22060
22480
  const projectRoot = path11.dirname(marvinDir);
22061
22481
  const actionTools = createSkillActionTools(skillsWithActions, { store, marvinDir, projectRoot });
@@ -24821,7 +25241,7 @@ function createProgram() {
24821
25241
  const program = new Command();
24822
25242
  program.name("marvin").description(
24823
25243
  "AI-powered product development assistant with Product Owner, Delivery Manager, and Technical Lead personas"
24824
- ).version("0.4.4");
25244
+ ).version("0.4.5");
24825
25245
  program.command("init").description("Initialize a new Marvin project in the current directory").action(async () => {
24826
25246
  await initCommand();
24827
25247
  });