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.d.ts +6 -2
- package/dist/index.js +585 -165
- package/dist/index.js.map +1 -1
- package/dist/marvin-serve.js +440 -19
- package/dist/marvin-serve.js.map +1 -1
- package/dist/marvin.js +443 -21
- package/dist/marvin.js.map +1 -1
- package/package.json +1 -1
package/dist/marvin.js
CHANGED
|
@@ -18354,6 +18354,204 @@ function getDiagramData(store) {
|
|
|
18354
18354
|
}
|
|
18355
18355
|
return { sprints, epics, features, statusCounts };
|
|
18356
18356
|
}
|
|
18357
|
+
function computeUrgency(dueDateStr, todayStr) {
|
|
18358
|
+
const due = new Date(dueDateStr).getTime();
|
|
18359
|
+
const today = new Date(todayStr).getTime();
|
|
18360
|
+
const diffDays = Math.floor((due - today) / 864e5);
|
|
18361
|
+
if (diffDays < 0) return "overdue";
|
|
18362
|
+
if (diffDays <= 3) return "due-3d";
|
|
18363
|
+
if (diffDays <= 7) return "due-7d";
|
|
18364
|
+
if (diffDays <= 14) return "upcoming";
|
|
18365
|
+
return "later";
|
|
18366
|
+
}
|
|
18367
|
+
var DONE_STATUSES = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
18368
|
+
function getUpcomingData(store) {
|
|
18369
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
18370
|
+
const allDocs = store.list();
|
|
18371
|
+
const docById = /* @__PURE__ */ new Map();
|
|
18372
|
+
for (const doc of allDocs) {
|
|
18373
|
+
docById.set(doc.frontmatter.id, doc);
|
|
18374
|
+
}
|
|
18375
|
+
const actions = allDocs.filter(
|
|
18376
|
+
(d) => d.frontmatter.type === "action" && !DONE_STATUSES.has(d.frontmatter.status)
|
|
18377
|
+
);
|
|
18378
|
+
const actionsWithDue = actions.filter((d) => d.frontmatter.dueDate);
|
|
18379
|
+
const sprints = allDocs.filter((d) => d.frontmatter.type === "sprint");
|
|
18380
|
+
const epics = allDocs.filter((d) => d.frontmatter.type === "epic");
|
|
18381
|
+
const tasks = allDocs.filter((d) => d.frontmatter.type === "task");
|
|
18382
|
+
const epicToTasks = /* @__PURE__ */ new Map();
|
|
18383
|
+
for (const task of tasks) {
|
|
18384
|
+
const tags = task.frontmatter.tags ?? [];
|
|
18385
|
+
for (const tag of tags) {
|
|
18386
|
+
if (tag.startsWith("epic:")) {
|
|
18387
|
+
const epicId = tag.slice(5);
|
|
18388
|
+
if (!epicToTasks.has(epicId)) epicToTasks.set(epicId, []);
|
|
18389
|
+
epicToTasks.get(epicId).push(task);
|
|
18390
|
+
}
|
|
18391
|
+
}
|
|
18392
|
+
}
|
|
18393
|
+
function getSprintTasks(sprintDoc) {
|
|
18394
|
+
const linkedEpics = normalizeLinkedEpics(sprintDoc.frontmatter.linkedEpics);
|
|
18395
|
+
const result = [];
|
|
18396
|
+
for (const epicId of linkedEpics) {
|
|
18397
|
+
const epicTasks = epicToTasks.get(epicId) ?? [];
|
|
18398
|
+
result.push(...epicTasks);
|
|
18399
|
+
}
|
|
18400
|
+
return result;
|
|
18401
|
+
}
|
|
18402
|
+
function countRelatedTasks(actionDoc) {
|
|
18403
|
+
const actionTags = actionDoc.frontmatter.tags ?? [];
|
|
18404
|
+
const relatedTaskIds = /* @__PURE__ */ new Set();
|
|
18405
|
+
for (const tag of actionTags) {
|
|
18406
|
+
if (tag.startsWith("sprint:")) {
|
|
18407
|
+
const sprintId = tag.slice(7);
|
|
18408
|
+
const sprint = docById.get(sprintId);
|
|
18409
|
+
if (sprint) {
|
|
18410
|
+
const sprintTaskDocs = getSprintTasks(sprint);
|
|
18411
|
+
for (const t of sprintTaskDocs) relatedTaskIds.add(t.frontmatter.id);
|
|
18412
|
+
}
|
|
18413
|
+
}
|
|
18414
|
+
}
|
|
18415
|
+
return relatedTaskIds.size;
|
|
18416
|
+
}
|
|
18417
|
+
const dueSoonActions = actionsWithDue.map((d) => ({
|
|
18418
|
+
id: d.frontmatter.id,
|
|
18419
|
+
title: d.frontmatter.title,
|
|
18420
|
+
status: d.frontmatter.status,
|
|
18421
|
+
owner: d.frontmatter.owner,
|
|
18422
|
+
dueDate: d.frontmatter.dueDate,
|
|
18423
|
+
urgency: computeUrgency(d.frontmatter.dueDate, today),
|
|
18424
|
+
relatedTaskCount: countRelatedTasks(d)
|
|
18425
|
+
})).sort((a, b) => a.dueDate.localeCompare(b.dueDate));
|
|
18426
|
+
const todayMs = new Date(today).getTime();
|
|
18427
|
+
const fourteenDaysMs = 14 * 864e5;
|
|
18428
|
+
const nearSprints = sprints.filter((s) => {
|
|
18429
|
+
const endDate = s.frontmatter.endDate;
|
|
18430
|
+
if (!endDate) return false;
|
|
18431
|
+
const endMs = new Date(endDate).getTime();
|
|
18432
|
+
const diff = endMs - todayMs;
|
|
18433
|
+
return diff >= 0 && diff <= fourteenDaysMs;
|
|
18434
|
+
});
|
|
18435
|
+
const taskSprintMap = /* @__PURE__ */ new Map();
|
|
18436
|
+
for (const sprint of nearSprints) {
|
|
18437
|
+
const sprintEnd = sprint.frontmatter.endDate;
|
|
18438
|
+
const sprintTaskDocs = getSprintTasks(sprint);
|
|
18439
|
+
for (const task of sprintTaskDocs) {
|
|
18440
|
+
if (DONE_STATUSES.has(task.frontmatter.status)) continue;
|
|
18441
|
+
const existing = taskSprintMap.get(task.frontmatter.id);
|
|
18442
|
+
if (!existing || sprintEnd < existing.sprintEnd) {
|
|
18443
|
+
taskSprintMap.set(task.frontmatter.id, { task, sprint, sprintEnd });
|
|
18444
|
+
}
|
|
18445
|
+
}
|
|
18446
|
+
}
|
|
18447
|
+
const dueSoonSprintTasks = [...taskSprintMap.values()].map(({ task, sprint, sprintEnd }) => ({
|
|
18448
|
+
id: task.frontmatter.id,
|
|
18449
|
+
title: task.frontmatter.title,
|
|
18450
|
+
status: task.frontmatter.status,
|
|
18451
|
+
sprintId: sprint.frontmatter.id,
|
|
18452
|
+
sprintTitle: sprint.frontmatter.title,
|
|
18453
|
+
sprintEndDate: sprintEnd,
|
|
18454
|
+
urgency: computeUrgency(sprintEnd, today)
|
|
18455
|
+
})).sort((a, b) => a.sprintEndDate.localeCompare(b.sprintEndDate));
|
|
18456
|
+
const openItems = allDocs.filter(
|
|
18457
|
+
(d) => ["action", "question", "task"].includes(d.frontmatter.type) && !DONE_STATUSES.has(d.frontmatter.status)
|
|
18458
|
+
);
|
|
18459
|
+
const fourteenDaysAgo = new Date(todayMs - fourteenDaysMs).toISOString().slice(0, 10);
|
|
18460
|
+
const recentMeetings = allDocs.filter(
|
|
18461
|
+
(d) => d.frontmatter.type === "meeting" && (d.frontmatter.updated ?? d.frontmatter.created) >= fourteenDaysAgo
|
|
18462
|
+
);
|
|
18463
|
+
const crossRefCounts = /* @__PURE__ */ new Map();
|
|
18464
|
+
for (const doc of allDocs) {
|
|
18465
|
+
const content = doc.content ?? "";
|
|
18466
|
+
for (const item of openItems) {
|
|
18467
|
+
if (doc.frontmatter.id === item.frontmatter.id) continue;
|
|
18468
|
+
if (content.includes(item.frontmatter.id)) {
|
|
18469
|
+
crossRefCounts.set(
|
|
18470
|
+
item.frontmatter.id,
|
|
18471
|
+
(crossRefCounts.get(item.frontmatter.id) ?? 0) + 1
|
|
18472
|
+
);
|
|
18473
|
+
}
|
|
18474
|
+
}
|
|
18475
|
+
}
|
|
18476
|
+
const activeSprints = sprints.filter((s) => {
|
|
18477
|
+
const status = s.frontmatter.status;
|
|
18478
|
+
if (status === "active") return true;
|
|
18479
|
+
const startDate = s.frontmatter.startDate;
|
|
18480
|
+
if (!startDate) return false;
|
|
18481
|
+
const startMs = new Date(startDate).getTime();
|
|
18482
|
+
const diff = startMs - todayMs;
|
|
18483
|
+
return diff >= 0 && diff <= fourteenDaysMs;
|
|
18484
|
+
});
|
|
18485
|
+
const activeSprintIds = new Set(activeSprints.map((s) => s.frontmatter.id));
|
|
18486
|
+
const activeEpicIds = /* @__PURE__ */ new Set();
|
|
18487
|
+
for (const s of activeSprints) {
|
|
18488
|
+
for (const epicId of normalizeLinkedEpics(s.frontmatter.linkedEpics)) {
|
|
18489
|
+
activeEpicIds.add(epicId);
|
|
18490
|
+
}
|
|
18491
|
+
}
|
|
18492
|
+
const trending = openItems.map((doc) => {
|
|
18493
|
+
const signals = [];
|
|
18494
|
+
let score = 0;
|
|
18495
|
+
const updated = doc.frontmatter.updated ?? doc.frontmatter.created;
|
|
18496
|
+
const ageDays = daysBetween(updated, today);
|
|
18497
|
+
const recencyPts = Math.max(0, Math.round(20 * (1 - ageDays / 30)));
|
|
18498
|
+
if (recencyPts > 0) {
|
|
18499
|
+
signals.push({ factor: "recency", points: recencyPts });
|
|
18500
|
+
score += recencyPts;
|
|
18501
|
+
}
|
|
18502
|
+
const tags = doc.frontmatter.tags ?? [];
|
|
18503
|
+
const linkedToActiveSprint = tags.some(
|
|
18504
|
+
(t) => t.startsWith("sprint:") && activeSprintIds.has(t.slice(7))
|
|
18505
|
+
);
|
|
18506
|
+
const linkedToActiveEpic = tags.some(
|
|
18507
|
+
(t) => t.startsWith("epic:") && activeEpicIds.has(t.slice(5))
|
|
18508
|
+
);
|
|
18509
|
+
if (linkedToActiveSprint) {
|
|
18510
|
+
signals.push({ factor: "sprint proximity", points: 25 });
|
|
18511
|
+
score += 25;
|
|
18512
|
+
} else if (linkedToActiveEpic) {
|
|
18513
|
+
signals.push({ factor: "sprint proximity", points: 15 });
|
|
18514
|
+
score += 15;
|
|
18515
|
+
}
|
|
18516
|
+
const mentionCount = recentMeetings.filter(
|
|
18517
|
+
(m) => (m.content ?? "").includes(doc.frontmatter.id)
|
|
18518
|
+
).length;
|
|
18519
|
+
if (mentionCount > 0) {
|
|
18520
|
+
const meetingPts = Math.min(15, mentionCount * 5);
|
|
18521
|
+
signals.push({ factor: "meeting mentions", points: meetingPts });
|
|
18522
|
+
score += meetingPts;
|
|
18523
|
+
}
|
|
18524
|
+
const priority = doc.frontmatter.priority?.toLowerCase();
|
|
18525
|
+
const priorityPts = priority === "critical" ? 15 : priority === "high" ? 10 : priority === "medium" ? 3 : 0;
|
|
18526
|
+
if (priorityPts > 0) {
|
|
18527
|
+
signals.push({ factor: "priority", points: priorityPts });
|
|
18528
|
+
score += priorityPts;
|
|
18529
|
+
}
|
|
18530
|
+
if (["action", "question"].includes(doc.frontmatter.type)) {
|
|
18531
|
+
const createdDays = daysBetween(doc.frontmatter.created, today);
|
|
18532
|
+
if (createdDays >= 14) {
|
|
18533
|
+
const agingPts = Math.min(10, Math.floor((createdDays - 14) / 7) * 3 + 5);
|
|
18534
|
+
signals.push({ factor: "aging", points: agingPts });
|
|
18535
|
+
score += agingPts;
|
|
18536
|
+
}
|
|
18537
|
+
}
|
|
18538
|
+
const refs = crossRefCounts.get(doc.frontmatter.id) ?? 0;
|
|
18539
|
+
if (refs > 0) {
|
|
18540
|
+
const crossRefPts = Math.min(15, refs * 5);
|
|
18541
|
+
signals.push({ factor: "cross-references", points: crossRefPts });
|
|
18542
|
+
score += crossRefPts;
|
|
18543
|
+
}
|
|
18544
|
+
return {
|
|
18545
|
+
id: doc.frontmatter.id,
|
|
18546
|
+
title: doc.frontmatter.title,
|
|
18547
|
+
type: doc.frontmatter.type,
|
|
18548
|
+
status: doc.frontmatter.status,
|
|
18549
|
+
score,
|
|
18550
|
+
signals
|
|
18551
|
+
};
|
|
18552
|
+
}).filter((item) => item.score > 0).sort((a, b) => b.score - a.score).slice(0, 15);
|
|
18553
|
+
return { dueSoonActions, dueSoonSprintTasks, trending };
|
|
18554
|
+
}
|
|
18357
18555
|
|
|
18358
18556
|
// src/web/templates/layout.ts
|
|
18359
18557
|
function escapeHtml(str) {
|
|
@@ -18477,6 +18675,7 @@ function inline(text) {
|
|
|
18477
18675
|
function layout(opts, body) {
|
|
18478
18676
|
const topItems = [
|
|
18479
18677
|
{ href: "/", label: "Overview" },
|
|
18678
|
+
{ href: "/upcoming", label: "Upcoming" },
|
|
18480
18679
|
{ href: "/timeline", label: "Timeline" },
|
|
18481
18680
|
{ href: "/board", label: "Board" },
|
|
18482
18681
|
{ href: "/gar", label: "GAR Report" },
|
|
@@ -19363,6 +19562,56 @@ tr:hover td {
|
|
|
19363
19562
|
fill: var(--bg) !important;
|
|
19364
19563
|
font-weight: 600;
|
|
19365
19564
|
}
|
|
19565
|
+
|
|
19566
|
+
/* Urgency row indicators */
|
|
19567
|
+
.urgency-row-overdue { border-left: 3px solid var(--red); }
|
|
19568
|
+
.urgency-row-due-3d { border-left: 3px solid var(--amber); }
|
|
19569
|
+
.urgency-row-due-7d { border-left: 3px solid #e2a308; }
|
|
19570
|
+
|
|
19571
|
+
/* Urgency badge pills */
|
|
19572
|
+
.urgency-badge-overdue { background: rgba(248, 113, 113, 0.15); color: var(--red); }
|
|
19573
|
+
.urgency-badge-due-3d { background: rgba(251, 191, 36, 0.15); color: var(--amber); }
|
|
19574
|
+
.urgency-badge-due-7d { background: rgba(226, 163, 8, 0.15); color: #e2a308; }
|
|
19575
|
+
.urgency-badge-upcoming { background: rgba(108, 140, 255, 0.15); color: var(--accent); }
|
|
19576
|
+
.urgency-badge-later { background: rgba(139, 143, 164, 0.1); color: var(--text-dim); }
|
|
19577
|
+
|
|
19578
|
+
/* Trending */
|
|
19579
|
+
.trending-rank {
|
|
19580
|
+
display: inline-flex;
|
|
19581
|
+
align-items: center;
|
|
19582
|
+
justify-content: center;
|
|
19583
|
+
width: 24px;
|
|
19584
|
+
height: 24px;
|
|
19585
|
+
border-radius: 50%;
|
|
19586
|
+
background: var(--bg-hover);
|
|
19587
|
+
font-size: 0.75rem;
|
|
19588
|
+
font-weight: 600;
|
|
19589
|
+
color: var(--text-dim);
|
|
19590
|
+
}
|
|
19591
|
+
|
|
19592
|
+
.trending-score {
|
|
19593
|
+
display: inline-block;
|
|
19594
|
+
padding: 0.15rem 0.6rem;
|
|
19595
|
+
border-radius: 999px;
|
|
19596
|
+
font-size: 0.7rem;
|
|
19597
|
+
font-weight: 700;
|
|
19598
|
+
background: rgba(108, 140, 255, 0.15);
|
|
19599
|
+
color: var(--accent);
|
|
19600
|
+
}
|
|
19601
|
+
|
|
19602
|
+
.signal-tag {
|
|
19603
|
+
display: inline-block;
|
|
19604
|
+
padding: 0.1rem 0.45rem;
|
|
19605
|
+
border-radius: 4px;
|
|
19606
|
+
font-size: 0.65rem;
|
|
19607
|
+
background: var(--bg-hover);
|
|
19608
|
+
color: var(--text-dim);
|
|
19609
|
+
margin-right: 0.25rem;
|
|
19610
|
+
margin-bottom: 0.15rem;
|
|
19611
|
+
white-space: nowrap;
|
|
19612
|
+
}
|
|
19613
|
+
|
|
19614
|
+
.text-dim { color: var(--text-dim); }
|
|
19366
19615
|
`;
|
|
19367
19616
|
}
|
|
19368
19617
|
|
|
@@ -19535,13 +19784,14 @@ function buildArtifactFlowchart(data) {
|
|
|
19535
19784
|
var svg = document.getElementById('flow-lines');
|
|
19536
19785
|
if (!container || !svg) return;
|
|
19537
19786
|
|
|
19538
|
-
// Build adjacency
|
|
19539
|
-
var
|
|
19787
|
+
// Build directed adjacency maps for traversal
|
|
19788
|
+
var fwd = {}; // from \u2192 [to] (Feature\u2192Epic, Epic\u2192Sprint)
|
|
19789
|
+
var bwd = {}; // to \u2192 [from] (Sprint\u2192Epic, Epic\u2192Feature)
|
|
19540
19790
|
edges.forEach(function(e) {
|
|
19541
|
-
if (!
|
|
19542
|
-
if (!
|
|
19543
|
-
|
|
19544
|
-
|
|
19791
|
+
if (!fwd[e.from]) fwd[e.from] = [];
|
|
19792
|
+
if (!bwd[e.to]) bwd[e.to] = [];
|
|
19793
|
+
fwd[e.from].push(e.to);
|
|
19794
|
+
bwd[e.to].push(e.from);
|
|
19545
19795
|
});
|
|
19546
19796
|
|
|
19547
19797
|
function drawLines() {
|
|
@@ -19574,14 +19824,28 @@ function buildArtifactFlowchart(data) {
|
|
|
19574
19824
|
});
|
|
19575
19825
|
}
|
|
19576
19826
|
|
|
19577
|
-
// Find
|
|
19827
|
+
// Find directly related nodes via directed traversal
|
|
19828
|
+
// Follows forward edges (Feature\u2192Epic\u2192Sprint) and backward edges
|
|
19829
|
+
// (Sprint\u2192Epic\u2192Feature) separately to avoid sideways expansion
|
|
19578
19830
|
function findConnected(startId) {
|
|
19579
19831
|
var visited = {};
|
|
19580
|
-
var queue = [startId];
|
|
19581
19832
|
visited[startId] = true;
|
|
19833
|
+
// Traverse forward (from\u2192to direction)
|
|
19834
|
+
var queue = [startId];
|
|
19582
19835
|
while (queue.length) {
|
|
19583
19836
|
var id = queue.shift();
|
|
19584
|
-
(
|
|
19837
|
+
(fwd[id] || []).forEach(function(neighbor) {
|
|
19838
|
+
if (!visited[neighbor]) {
|
|
19839
|
+
visited[neighbor] = true;
|
|
19840
|
+
queue.push(neighbor);
|
|
19841
|
+
}
|
|
19842
|
+
});
|
|
19843
|
+
}
|
|
19844
|
+
// Traverse backward (to\u2192from direction)
|
|
19845
|
+
queue = [startId];
|
|
19846
|
+
while (queue.length) {
|
|
19847
|
+
var id = queue.shift();
|
|
19848
|
+
(bwd[id] || []).forEach(function(neighbor) {
|
|
19585
19849
|
if (!visited[neighbor]) {
|
|
19586
19850
|
visited[neighbor] = true;
|
|
19587
19851
|
queue.push(neighbor);
|
|
@@ -20023,6 +20287,131 @@ function timelinePage(diagrams) {
|
|
|
20023
20287
|
`;
|
|
20024
20288
|
}
|
|
20025
20289
|
|
|
20290
|
+
// src/web/templates/pages/upcoming.ts
|
|
20291
|
+
function urgencyBadge(tier) {
|
|
20292
|
+
const labels = {
|
|
20293
|
+
overdue: "Overdue",
|
|
20294
|
+
"due-3d": "Due in 3d",
|
|
20295
|
+
"due-7d": "Due in 7d",
|
|
20296
|
+
upcoming: "Upcoming",
|
|
20297
|
+
later: "Later"
|
|
20298
|
+
};
|
|
20299
|
+
return `<span class="badge urgency-badge-${tier}">${labels[tier]}</span>`;
|
|
20300
|
+
}
|
|
20301
|
+
function urgencyRowClass(tier) {
|
|
20302
|
+
if (tier === "overdue") return " urgency-row-overdue";
|
|
20303
|
+
if (tier === "due-3d") return " urgency-row-due-3d";
|
|
20304
|
+
if (tier === "due-7d") return " urgency-row-due-7d";
|
|
20305
|
+
return "";
|
|
20306
|
+
}
|
|
20307
|
+
function upcomingPage(data) {
|
|
20308
|
+
const hasActions = data.dueSoonActions.length > 0;
|
|
20309
|
+
const hasSprintTasks = data.dueSoonSprintTasks.length > 0;
|
|
20310
|
+
const hasTrending = data.trending.length > 0;
|
|
20311
|
+
const actionsTable = hasActions ? `
|
|
20312
|
+
<h3 class="section-title">Due Soon \u2014 Actions</h3>
|
|
20313
|
+
<div class="table-wrap">
|
|
20314
|
+
<table>
|
|
20315
|
+
<thead>
|
|
20316
|
+
<tr>
|
|
20317
|
+
<th>ID</th>
|
|
20318
|
+
<th>Title</th>
|
|
20319
|
+
<th>Status</th>
|
|
20320
|
+
<th>Owner</th>
|
|
20321
|
+
<th>Due Date</th>
|
|
20322
|
+
<th>Urgency</th>
|
|
20323
|
+
<th>Tasks</th>
|
|
20324
|
+
</tr>
|
|
20325
|
+
</thead>
|
|
20326
|
+
<tbody>
|
|
20327
|
+
${data.dueSoonActions.map(
|
|
20328
|
+
(a) => `
|
|
20329
|
+
<tr class="${urgencyRowClass(a.urgency)}">
|
|
20330
|
+
<td><a href="/docs/action/${escapeHtml(a.id)}">${escapeHtml(a.id)}</a></td>
|
|
20331
|
+
<td>${escapeHtml(a.title)}</td>
|
|
20332
|
+
<td>${statusBadge(a.status)}</td>
|
|
20333
|
+
<td>${a.owner ? escapeHtml(a.owner) : '<span class="text-dim">\u2014</span>'}</td>
|
|
20334
|
+
<td>${formatDate(a.dueDate)}</td>
|
|
20335
|
+
<td>${urgencyBadge(a.urgency)}</td>
|
|
20336
|
+
<td>${a.relatedTaskCount > 0 ? a.relatedTaskCount : "\u2014"}</td>
|
|
20337
|
+
</tr>`
|
|
20338
|
+
).join("")}
|
|
20339
|
+
</tbody>
|
|
20340
|
+
</table>
|
|
20341
|
+
</div>` : "";
|
|
20342
|
+
const sprintTasksTable = hasSprintTasks ? `
|
|
20343
|
+
<h3 class="section-title">Due Soon \u2014 Sprint Tasks</h3>
|
|
20344
|
+
<div class="table-wrap">
|
|
20345
|
+
<table>
|
|
20346
|
+
<thead>
|
|
20347
|
+
<tr>
|
|
20348
|
+
<th>ID</th>
|
|
20349
|
+
<th>Title</th>
|
|
20350
|
+
<th>Status</th>
|
|
20351
|
+
<th>Sprint</th>
|
|
20352
|
+
<th>Sprint Ends</th>
|
|
20353
|
+
<th>Urgency</th>
|
|
20354
|
+
</tr>
|
|
20355
|
+
</thead>
|
|
20356
|
+
<tbody>
|
|
20357
|
+
${data.dueSoonSprintTasks.map(
|
|
20358
|
+
(t) => `
|
|
20359
|
+
<tr class="${urgencyRowClass(t.urgency)}">
|
|
20360
|
+
<td><a href="/docs/task/${escapeHtml(t.id)}">${escapeHtml(t.id)}</a></td>
|
|
20361
|
+
<td>${escapeHtml(t.title)}</td>
|
|
20362
|
+
<td>${statusBadge(t.status)}</td>
|
|
20363
|
+
<td><a href="/docs/sprint/${escapeHtml(t.sprintId)}">${escapeHtml(t.sprintId)}</a></td>
|
|
20364
|
+
<td>${formatDate(t.sprintEndDate)}</td>
|
|
20365
|
+
<td>${urgencyBadge(t.urgency)}</td>
|
|
20366
|
+
</tr>`
|
|
20367
|
+
).join("")}
|
|
20368
|
+
</tbody>
|
|
20369
|
+
</table>
|
|
20370
|
+
</div>` : "";
|
|
20371
|
+
const trendingTable = hasTrending ? `
|
|
20372
|
+
<h3 class="section-title">Trending</h3>
|
|
20373
|
+
<div class="table-wrap">
|
|
20374
|
+
<table>
|
|
20375
|
+
<thead>
|
|
20376
|
+
<tr>
|
|
20377
|
+
<th>#</th>
|
|
20378
|
+
<th>ID</th>
|
|
20379
|
+
<th>Title</th>
|
|
20380
|
+
<th>Type</th>
|
|
20381
|
+
<th>Status</th>
|
|
20382
|
+
<th>Score</th>
|
|
20383
|
+
<th>Signals</th>
|
|
20384
|
+
</tr>
|
|
20385
|
+
</thead>
|
|
20386
|
+
<tbody>
|
|
20387
|
+
${data.trending.map(
|
|
20388
|
+
(t, i) => `
|
|
20389
|
+
<tr>
|
|
20390
|
+
<td><span class="trending-rank">${i + 1}</span></td>
|
|
20391
|
+
<td><a href="/docs/${escapeHtml(t.type)}/${escapeHtml(t.id)}">${escapeHtml(t.id)}</a></td>
|
|
20392
|
+
<td>${escapeHtml(t.title)}</td>
|
|
20393
|
+
<td>${escapeHtml(typeLabel(t.type))}</td>
|
|
20394
|
+
<td>${statusBadge(t.status)}</td>
|
|
20395
|
+
<td><span class="trending-score">${t.score}</span></td>
|
|
20396
|
+
<td>${t.signals.map((s) => `<span class="signal-tag">${escapeHtml(s.factor)} +${s.points}</span>`).join(" ")}</td>
|
|
20397
|
+
</tr>`
|
|
20398
|
+
).join("")}
|
|
20399
|
+
</tbody>
|
|
20400
|
+
</table>
|
|
20401
|
+
</div>` : "";
|
|
20402
|
+
const emptyState = !hasActions && !hasSprintTasks && !hasTrending ? '<div class="empty"><p>No upcoming items or trending activity found.</p></div>' : "";
|
|
20403
|
+
return `
|
|
20404
|
+
<div class="page-header">
|
|
20405
|
+
<h2>Upcoming</h2>
|
|
20406
|
+
<div class="subtitle">Time-sensitive items and trending activity</div>
|
|
20407
|
+
</div>
|
|
20408
|
+
${actionsTable}
|
|
20409
|
+
${sprintTasksTable}
|
|
20410
|
+
${trendingTable}
|
|
20411
|
+
${emptyState}
|
|
20412
|
+
`;
|
|
20413
|
+
}
|
|
20414
|
+
|
|
20026
20415
|
// src/web/router.ts
|
|
20027
20416
|
function handleRequest(req, res, store, projectName, navGroups) {
|
|
20028
20417
|
const parsed = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
@@ -20063,6 +20452,12 @@ function handleRequest(req, res, store, projectName, navGroups) {
|
|
|
20063
20452
|
respond(res, layout({ title: "Health Check", activePath: "/health", projectName, navGroups }, body));
|
|
20064
20453
|
return;
|
|
20065
20454
|
}
|
|
20455
|
+
if (pathname === "/upcoming") {
|
|
20456
|
+
const data = getUpcomingData(store);
|
|
20457
|
+
const body = upcomingPage(data);
|
|
20458
|
+
respond(res, layout({ title: "Upcoming", activePath: "/upcoming", projectName, navGroups }, body));
|
|
20459
|
+
return;
|
|
20460
|
+
}
|
|
20066
20461
|
const boardMatch = pathname.match(/^\/board(?:\/([^/]+))?$/);
|
|
20067
20462
|
if (boardMatch) {
|
|
20068
20463
|
const type = boardMatch[1];
|
|
@@ -20272,8 +20667,9 @@ function findByJiraKey(store, jiraKey) {
|
|
|
20272
20667
|
const docs = store.list({ type: JIRA_TYPE });
|
|
20273
20668
|
return docs.find((d) => d.frontmatter.jiraKey === jiraKey);
|
|
20274
20669
|
}
|
|
20275
|
-
function createJiraTools(store) {
|
|
20670
|
+
function createJiraTools(store, projectConfig) {
|
|
20276
20671
|
const jiraUserConfig = loadUserConfig().jira;
|
|
20672
|
+
const defaultProjectKey = projectConfig?.jira?.projectKey;
|
|
20277
20673
|
return [
|
|
20278
20674
|
// --- Local read tools ---
|
|
20279
20675
|
tool20(
|
|
@@ -20437,10 +20833,22 @@ function createJiraTools(store) {
|
|
|
20437
20833
|
"Create a Jira issue from any Marvin artifact (D/A/Q/F/E) and create a tracking JI-xxx document",
|
|
20438
20834
|
{
|
|
20439
20835
|
artifactId: external_exports.string().describe("Marvin artifact ID (e.g. 'D-001', 'F-003', 'E-002')"),
|
|
20440
|
-
projectKey: external_exports.string().describe("Jira project key (e.g. 'PROJ')"),
|
|
20836
|
+
projectKey: external_exports.string().optional().describe("Jira project key (e.g. 'PROJ'). Falls back to jira.projectKey from .marvin/config.yaml if not provided."),
|
|
20441
20837
|
issueType: external_exports.enum(["Story", "Task", "Bug", "Epic"]).optional().describe("Jira issue type (default: 'Task')")
|
|
20442
20838
|
},
|
|
20443
20839
|
async (args) => {
|
|
20840
|
+
const resolvedProjectKey = args.projectKey ?? defaultProjectKey;
|
|
20841
|
+
if (!resolvedProjectKey) {
|
|
20842
|
+
return {
|
|
20843
|
+
content: [
|
|
20844
|
+
{
|
|
20845
|
+
type: "text",
|
|
20846
|
+
text: "No projectKey provided and no default configured. Either pass projectKey or set jira.projectKey in .marvin/config.yaml."
|
|
20847
|
+
}
|
|
20848
|
+
],
|
|
20849
|
+
isError: true
|
|
20850
|
+
};
|
|
20851
|
+
}
|
|
20444
20852
|
const jira = createJiraClient(jiraUserConfig);
|
|
20445
20853
|
if (!jira) return jiraNotConfiguredError();
|
|
20446
20854
|
const artifact = store.get(args.artifactId);
|
|
@@ -20460,7 +20868,7 @@ function createJiraTools(store) {
|
|
|
20460
20868
|
`Status: ${artifact.frontmatter.status}`
|
|
20461
20869
|
].join("\n");
|
|
20462
20870
|
const jiraResult = await jira.client.createIssue({
|
|
20463
|
-
project: { key:
|
|
20871
|
+
project: { key: resolvedProjectKey },
|
|
20464
20872
|
summary: artifact.frontmatter.title,
|
|
20465
20873
|
description,
|
|
20466
20874
|
issuetype: { name: args.issueType ?? "Task" }
|
|
@@ -20601,14 +21009,14 @@ var jiraSkill = {
|
|
|
20601
21009
|
documentTypeRegistrations: [
|
|
20602
21010
|
{ type: "jira-issue", dirName: "jira-issues", idPrefix: "JI" }
|
|
20603
21011
|
],
|
|
20604
|
-
tools: (store) => createJiraTools(store),
|
|
21012
|
+
tools: (store, projectConfig) => createJiraTools(store, projectConfig),
|
|
20605
21013
|
promptFragments: {
|
|
20606
21014
|
"product-owner": `You have the **Jira Integration** skill. You can pull issues from Jira and push Marvin artifacts to Jira.
|
|
20607
21015
|
|
|
20608
21016
|
**Available tools:**
|
|
20609
21017
|
- \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues
|
|
20610
21018
|
- \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import issues from Jira by key or JQL query
|
|
20611
|
-
- \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, feature, etc.)
|
|
21019
|
+
- \`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\`.
|
|
20612
21020
|
- \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
|
|
20613
21021
|
- \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx
|
|
20614
21022
|
|
|
@@ -20622,7 +21030,7 @@ var jiraSkill = {
|
|
|
20622
21030
|
**Available tools:**
|
|
20623
21031
|
- \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues
|
|
20624
21032
|
- \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import issues from Jira by key or JQL query
|
|
20625
|
-
- \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, action, epic, task, etc.)
|
|
21033
|
+
- \`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\`.
|
|
20626
21034
|
- \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
|
|
20627
21035
|
- \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx
|
|
20628
21036
|
|
|
@@ -20636,7 +21044,7 @@ var jiraSkill = {
|
|
|
20636
21044
|
**Available tools:**
|
|
20637
21045
|
- \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues
|
|
20638
21046
|
- \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import issues from Jira by key or JQL query
|
|
20639
|
-
- \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, action, etc.)
|
|
21047
|
+
- \`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\`.
|
|
20640
21048
|
- \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
|
|
20641
21049
|
- \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx
|
|
20642
21050
|
|
|
@@ -21214,12 +21622,12 @@ function collectSkillRegistrations(skillIds, allSkills) {
|
|
|
21214
21622
|
}
|
|
21215
21623
|
return registrations;
|
|
21216
21624
|
}
|
|
21217
|
-
function getSkillTools(skillIds, allSkills, store) {
|
|
21625
|
+
function getSkillTools(skillIds, allSkills, store, projectConfig) {
|
|
21218
21626
|
const tools = [];
|
|
21219
21627
|
for (const id of skillIds) {
|
|
21220
21628
|
const skill = allSkills.get(id);
|
|
21221
21629
|
if (skill?.tools) {
|
|
21222
|
-
tools.push(...skill.tools(store));
|
|
21630
|
+
tools.push(...skill.tools(store, projectConfig));
|
|
21223
21631
|
}
|
|
21224
21632
|
}
|
|
21225
21633
|
return tools;
|
|
@@ -21459,6 +21867,7 @@ function createWebTools(store, projectName, navGroups) {
|
|
|
21459
21867
|
const base = `http://localhost:${runningServer.port}`;
|
|
21460
21868
|
const urls = {
|
|
21461
21869
|
overview: base,
|
|
21870
|
+
upcoming: `${base}/upcoming`,
|
|
21462
21871
|
gar: `${base}/gar`,
|
|
21463
21872
|
board: `${base}/board`
|
|
21464
21873
|
};
|
|
@@ -21532,6 +21941,18 @@ function createWebTools(store, projectName, navGroups) {
|
|
|
21532
21941
|
};
|
|
21533
21942
|
},
|
|
21534
21943
|
{ annotations: { readOnlyHint: true } }
|
|
21944
|
+
),
|
|
21945
|
+
tool22(
|
|
21946
|
+
"get_dashboard_upcoming",
|
|
21947
|
+
"Get upcoming data: due-soon actions and sprint tasks, plus trending items scored by relevance signals. Works without the web server running.",
|
|
21948
|
+
{},
|
|
21949
|
+
async () => {
|
|
21950
|
+
const data = getUpcomingData(store);
|
|
21951
|
+
return {
|
|
21952
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
21953
|
+
};
|
|
21954
|
+
},
|
|
21955
|
+
{ annotations: { readOnlyHint: true } }
|
|
21535
21956
|
)
|
|
21536
21957
|
];
|
|
21537
21958
|
}
|
|
@@ -21715,7 +22136,7 @@ async function startSession(options) {
|
|
|
21715
22136
|
const manifest = hasSourcesDir ? new SourceManifestManager(marvinDir) : void 0;
|
|
21716
22137
|
const pluginTools = plugin ? getPluginTools(plugin, store, marvinDir) : [];
|
|
21717
22138
|
const pluginPromptFragment = plugin ? getPluginPromptFragment(plugin, persona.id) : void 0;
|
|
21718
|
-
const codeSkillTools = getSkillTools(skillIds, allSkills, store);
|
|
22139
|
+
const codeSkillTools = getSkillTools(skillIds, allSkills, store, config2.project);
|
|
21719
22140
|
const skillAgents = getSkillAgentDefinitions(skillIds, allSkills);
|
|
21720
22141
|
const skillPromptFragment = getSkillPromptFragment(skillIds, allSkills, persona.id);
|
|
21721
22142
|
const allSkillIds = [...allSkills.keys()];
|
|
@@ -21827,6 +22248,7 @@ Marvin \u2014 ${persona.name}
|
|
|
21827
22248
|
"mcp__marvin-governance__get_dashboard_overview",
|
|
21828
22249
|
"mcp__marvin-governance__get_dashboard_gar",
|
|
21829
22250
|
"mcp__marvin-governance__get_dashboard_board",
|
|
22251
|
+
"mcp__marvin-governance__get_dashboard_upcoming",
|
|
21830
22252
|
...pluginTools.map((t) => `mcp__marvin-governance__${t.name}`),
|
|
21831
22253
|
...codeSkillTools.map((t) => `mcp__marvin-governance__${t.name}`)
|
|
21832
22254
|
]
|
|
@@ -23156,7 +23578,7 @@ function collectTools(marvinDir) {
|
|
|
23156
23578
|
const sessionStore = new SessionStore(marvinDir);
|
|
23157
23579
|
const allSkills = loadAllSkills(marvinDir);
|
|
23158
23580
|
const allSkillIds = [...allSkills.keys()];
|
|
23159
|
-
const codeSkillTools = getSkillTools(allSkillIds, allSkills, store);
|
|
23581
|
+
const codeSkillTools = getSkillTools(allSkillIds, allSkills, store, config2);
|
|
23160
23582
|
const skillsWithActions = allSkillIds.map((id) => allSkills.get(id)).filter((s) => s.actions && s.actions.length > 0);
|
|
23161
23583
|
const projectRoot = path15.dirname(marvinDir);
|
|
23162
23584
|
const actionTools = createSkillActionTools(skillsWithActions, { store, marvinDir, projectRoot });
|
|
@@ -24815,7 +25237,7 @@ function createProgram() {
|
|
|
24815
25237
|
const program2 = new Command();
|
|
24816
25238
|
program2.name("marvin").description(
|
|
24817
25239
|
"AI-powered product development assistant with Product Owner, Delivery Manager, and Technical Lead personas"
|
|
24818
|
-
).version("0.4.
|
|
25240
|
+
).version("0.4.5");
|
|
24819
25241
|
program2.command("init").description("Initialize a new Marvin project in the current directory").action(async () => {
|
|
24820
25242
|
await initCommand();
|
|
24821
25243
|
});
|