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/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 map (bidirectional) for traversal
19539
- var adj = {};
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 (!adj[e.from]) adj[e.from] = [];
19542
- if (!adj[e.to]) adj[e.to] = [];
19543
- adj[e.from].push(e.to);
19544
- adj[e.to].push(e.from);
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 all nodes reachable from a starting node
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
- (adj[id] || []).forEach(function(neighbor) {
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: args.projectKey },
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.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
  });