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.
@@ -18054,8 +18054,9 @@ function findByJiraKey(store, jiraKey) {
18054
18054
  const docs = store.list({ type: JIRA_TYPE });
18055
18055
  return docs.find((d) => d.frontmatter.jiraKey === jiraKey);
18056
18056
  }
18057
- function createJiraTools(store) {
18057
+ function createJiraTools(store, projectConfig) {
18058
18058
  const jiraUserConfig = loadUserConfig().jira;
18059
+ const defaultProjectKey = projectConfig?.jira?.projectKey;
18059
18060
  return [
18060
18061
  // --- Local read tools ---
18061
18062
  tool20(
@@ -18219,10 +18220,22 @@ function createJiraTools(store) {
18219
18220
  "Create a Jira issue from any Marvin artifact (D/A/Q/F/E) and create a tracking JI-xxx document",
18220
18221
  {
18221
18222
  artifactId: external_exports.string().describe("Marvin artifact ID (e.g. 'D-001', 'F-003', 'E-002')"),
18222
- projectKey: external_exports.string().describe("Jira project key (e.g. 'PROJ')"),
18223
+ projectKey: external_exports.string().optional().describe("Jira project key (e.g. 'PROJ'). Falls back to jira.projectKey from .marvin/config.yaml if not provided."),
18223
18224
  issueType: external_exports.enum(["Story", "Task", "Bug", "Epic"]).optional().describe("Jira issue type (default: 'Task')")
18224
18225
  },
18225
18226
  async (args) => {
18227
+ const resolvedProjectKey = args.projectKey ?? defaultProjectKey;
18228
+ if (!resolvedProjectKey) {
18229
+ return {
18230
+ content: [
18231
+ {
18232
+ type: "text",
18233
+ text: "No projectKey provided and no default configured. Either pass projectKey or set jira.projectKey in .marvin/config.yaml."
18234
+ }
18235
+ ],
18236
+ isError: true
18237
+ };
18238
+ }
18226
18239
  const jira = createJiraClient(jiraUserConfig);
18227
18240
  if (!jira) return jiraNotConfiguredError();
18228
18241
  const artifact = store.get(args.artifactId);
@@ -18242,7 +18255,7 @@ function createJiraTools(store) {
18242
18255
  `Status: ${artifact.frontmatter.status}`
18243
18256
  ].join("\n");
18244
18257
  const jiraResult = await jira.client.createIssue({
18245
- project: { key: args.projectKey },
18258
+ project: { key: resolvedProjectKey },
18246
18259
  summary: artifact.frontmatter.title,
18247
18260
  description,
18248
18261
  issuetype: { name: args.issueType ?? "Task" }
@@ -18383,14 +18396,14 @@ var jiraSkill = {
18383
18396
  documentTypeRegistrations: [
18384
18397
  { type: "jira-issue", dirName: "jira-issues", idPrefix: "JI" }
18385
18398
  ],
18386
- tools: (store) => createJiraTools(store),
18399
+ tools: (store, projectConfig) => createJiraTools(store, projectConfig),
18387
18400
  promptFragments: {
18388
18401
  "product-owner": `You have the **Jira Integration** skill. You can pull issues from Jira and push Marvin artifacts to Jira.
18389
18402
 
18390
18403
  **Available tools:**
18391
18404
  - \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues
18392
18405
  - \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import issues from Jira by key or JQL query
18393
- - \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, feature, etc.)
18406
+ - \`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\`.
18394
18407
  - \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
18395
18408
  - \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx
18396
18409
 
@@ -18404,7 +18417,7 @@ var jiraSkill = {
18404
18417
  **Available tools:**
18405
18418
  - \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues
18406
18419
  - \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import issues from Jira by key or JQL query
18407
- - \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, action, epic, task, etc.)
18420
+ - \`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\`.
18408
18421
  - \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
18409
18422
  - \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx
18410
18423
 
@@ -18418,7 +18431,7 @@ var jiraSkill = {
18418
18431
  **Available tools:**
18419
18432
  - \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues
18420
18433
  - \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import issues from Jira by key or JQL query
18421
- - \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, action, etc.)
18434
+ - \`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\`.
18422
18435
  - \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
18423
18436
  - \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx
18424
18437
 
@@ -18979,12 +18992,12 @@ function collectSkillRegistrations(skillIds, allSkills) {
18979
18992
  }
18980
18993
  return registrations;
18981
18994
  }
18982
- function getSkillTools(skillIds, allSkills, store) {
18995
+ function getSkillTools(skillIds, allSkills, store, projectConfig) {
18983
18996
  const tools = [];
18984
18997
  for (const id of skillIds) {
18985
18998
  const skill = allSkills.get(id);
18986
18999
  if (skill?.tools) {
18987
- tools.push(...skill.tools(store));
19000
+ tools.push(...skill.tools(store, projectConfig));
18988
19001
  }
18989
19002
  }
18990
19003
  return tools;
@@ -19129,6 +19142,204 @@ function getDiagramData(store) {
19129
19142
  }
19130
19143
  return { sprints, epics, features, statusCounts };
19131
19144
  }
19145
+ function computeUrgency(dueDateStr, todayStr) {
19146
+ const due = new Date(dueDateStr).getTime();
19147
+ const today = new Date(todayStr).getTime();
19148
+ const diffDays = Math.floor((due - today) / 864e5);
19149
+ if (diffDays < 0) return "overdue";
19150
+ if (diffDays <= 3) return "due-3d";
19151
+ if (diffDays <= 7) return "due-7d";
19152
+ if (diffDays <= 14) return "upcoming";
19153
+ return "later";
19154
+ }
19155
+ var DONE_STATUSES = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
19156
+ function getUpcomingData(store) {
19157
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
19158
+ const allDocs = store.list();
19159
+ const docById = /* @__PURE__ */ new Map();
19160
+ for (const doc of allDocs) {
19161
+ docById.set(doc.frontmatter.id, doc);
19162
+ }
19163
+ const actions = allDocs.filter(
19164
+ (d) => d.frontmatter.type === "action" && !DONE_STATUSES.has(d.frontmatter.status)
19165
+ );
19166
+ const actionsWithDue = actions.filter((d) => d.frontmatter.dueDate);
19167
+ const sprints = allDocs.filter((d) => d.frontmatter.type === "sprint");
19168
+ const epics = allDocs.filter((d) => d.frontmatter.type === "epic");
19169
+ const tasks = allDocs.filter((d) => d.frontmatter.type === "task");
19170
+ const epicToTasks = /* @__PURE__ */ new Map();
19171
+ for (const task of tasks) {
19172
+ const tags = task.frontmatter.tags ?? [];
19173
+ for (const tag of tags) {
19174
+ if (tag.startsWith("epic:")) {
19175
+ const epicId = tag.slice(5);
19176
+ if (!epicToTasks.has(epicId)) epicToTasks.set(epicId, []);
19177
+ epicToTasks.get(epicId).push(task);
19178
+ }
19179
+ }
19180
+ }
19181
+ function getSprintTasks(sprintDoc) {
19182
+ const linkedEpics = normalizeLinkedEpics(sprintDoc.frontmatter.linkedEpics);
19183
+ const result = [];
19184
+ for (const epicId of linkedEpics) {
19185
+ const epicTasks = epicToTasks.get(epicId) ?? [];
19186
+ result.push(...epicTasks);
19187
+ }
19188
+ return result;
19189
+ }
19190
+ function countRelatedTasks(actionDoc) {
19191
+ const actionTags = actionDoc.frontmatter.tags ?? [];
19192
+ const relatedTaskIds = /* @__PURE__ */ new Set();
19193
+ for (const tag of actionTags) {
19194
+ if (tag.startsWith("sprint:")) {
19195
+ const sprintId = tag.slice(7);
19196
+ const sprint = docById.get(sprintId);
19197
+ if (sprint) {
19198
+ const sprintTaskDocs = getSprintTasks(sprint);
19199
+ for (const t of sprintTaskDocs) relatedTaskIds.add(t.frontmatter.id);
19200
+ }
19201
+ }
19202
+ }
19203
+ return relatedTaskIds.size;
19204
+ }
19205
+ const dueSoonActions = actionsWithDue.map((d) => ({
19206
+ id: d.frontmatter.id,
19207
+ title: d.frontmatter.title,
19208
+ status: d.frontmatter.status,
19209
+ owner: d.frontmatter.owner,
19210
+ dueDate: d.frontmatter.dueDate,
19211
+ urgency: computeUrgency(d.frontmatter.dueDate, today),
19212
+ relatedTaskCount: countRelatedTasks(d)
19213
+ })).sort((a, b) => a.dueDate.localeCompare(b.dueDate));
19214
+ const todayMs = new Date(today).getTime();
19215
+ const fourteenDaysMs = 14 * 864e5;
19216
+ const nearSprints = sprints.filter((s) => {
19217
+ const endDate = s.frontmatter.endDate;
19218
+ if (!endDate) return false;
19219
+ const endMs = new Date(endDate).getTime();
19220
+ const diff = endMs - todayMs;
19221
+ return diff >= 0 && diff <= fourteenDaysMs;
19222
+ });
19223
+ const taskSprintMap = /* @__PURE__ */ new Map();
19224
+ for (const sprint of nearSprints) {
19225
+ const sprintEnd = sprint.frontmatter.endDate;
19226
+ const sprintTaskDocs = getSprintTasks(sprint);
19227
+ for (const task of sprintTaskDocs) {
19228
+ if (DONE_STATUSES.has(task.frontmatter.status)) continue;
19229
+ const existing = taskSprintMap.get(task.frontmatter.id);
19230
+ if (!existing || sprintEnd < existing.sprintEnd) {
19231
+ taskSprintMap.set(task.frontmatter.id, { task, sprint, sprintEnd });
19232
+ }
19233
+ }
19234
+ }
19235
+ const dueSoonSprintTasks = [...taskSprintMap.values()].map(({ task, sprint, sprintEnd }) => ({
19236
+ id: task.frontmatter.id,
19237
+ title: task.frontmatter.title,
19238
+ status: task.frontmatter.status,
19239
+ sprintId: sprint.frontmatter.id,
19240
+ sprintTitle: sprint.frontmatter.title,
19241
+ sprintEndDate: sprintEnd,
19242
+ urgency: computeUrgency(sprintEnd, today)
19243
+ })).sort((a, b) => a.sprintEndDate.localeCompare(b.sprintEndDate));
19244
+ const openItems = allDocs.filter(
19245
+ (d) => ["action", "question", "task"].includes(d.frontmatter.type) && !DONE_STATUSES.has(d.frontmatter.status)
19246
+ );
19247
+ const fourteenDaysAgo = new Date(todayMs - fourteenDaysMs).toISOString().slice(0, 10);
19248
+ const recentMeetings = allDocs.filter(
19249
+ (d) => d.frontmatter.type === "meeting" && (d.frontmatter.updated ?? d.frontmatter.created) >= fourteenDaysAgo
19250
+ );
19251
+ const crossRefCounts = /* @__PURE__ */ new Map();
19252
+ for (const doc of allDocs) {
19253
+ const content = doc.content ?? "";
19254
+ for (const item of openItems) {
19255
+ if (doc.frontmatter.id === item.frontmatter.id) continue;
19256
+ if (content.includes(item.frontmatter.id)) {
19257
+ crossRefCounts.set(
19258
+ item.frontmatter.id,
19259
+ (crossRefCounts.get(item.frontmatter.id) ?? 0) + 1
19260
+ );
19261
+ }
19262
+ }
19263
+ }
19264
+ const activeSprints = sprints.filter((s) => {
19265
+ const status = s.frontmatter.status;
19266
+ if (status === "active") return true;
19267
+ const startDate = s.frontmatter.startDate;
19268
+ if (!startDate) return false;
19269
+ const startMs = new Date(startDate).getTime();
19270
+ const diff = startMs - todayMs;
19271
+ return diff >= 0 && diff <= fourteenDaysMs;
19272
+ });
19273
+ const activeSprintIds = new Set(activeSprints.map((s) => s.frontmatter.id));
19274
+ const activeEpicIds = /* @__PURE__ */ new Set();
19275
+ for (const s of activeSprints) {
19276
+ for (const epicId of normalizeLinkedEpics(s.frontmatter.linkedEpics)) {
19277
+ activeEpicIds.add(epicId);
19278
+ }
19279
+ }
19280
+ const trending = openItems.map((doc) => {
19281
+ const signals = [];
19282
+ let score = 0;
19283
+ const updated = doc.frontmatter.updated ?? doc.frontmatter.created;
19284
+ const ageDays = daysBetween(updated, today);
19285
+ const recencyPts = Math.max(0, Math.round(20 * (1 - ageDays / 30)));
19286
+ if (recencyPts > 0) {
19287
+ signals.push({ factor: "recency", points: recencyPts });
19288
+ score += recencyPts;
19289
+ }
19290
+ const tags = doc.frontmatter.tags ?? [];
19291
+ const linkedToActiveSprint = tags.some(
19292
+ (t) => t.startsWith("sprint:") && activeSprintIds.has(t.slice(7))
19293
+ );
19294
+ const linkedToActiveEpic = tags.some(
19295
+ (t) => t.startsWith("epic:") && activeEpicIds.has(t.slice(5))
19296
+ );
19297
+ if (linkedToActiveSprint) {
19298
+ signals.push({ factor: "sprint proximity", points: 25 });
19299
+ score += 25;
19300
+ } else if (linkedToActiveEpic) {
19301
+ signals.push({ factor: "sprint proximity", points: 15 });
19302
+ score += 15;
19303
+ }
19304
+ const mentionCount = recentMeetings.filter(
19305
+ (m) => (m.content ?? "").includes(doc.frontmatter.id)
19306
+ ).length;
19307
+ if (mentionCount > 0) {
19308
+ const meetingPts = Math.min(15, mentionCount * 5);
19309
+ signals.push({ factor: "meeting mentions", points: meetingPts });
19310
+ score += meetingPts;
19311
+ }
19312
+ const priority = doc.frontmatter.priority?.toLowerCase();
19313
+ const priorityPts = priority === "critical" ? 15 : priority === "high" ? 10 : priority === "medium" ? 3 : 0;
19314
+ if (priorityPts > 0) {
19315
+ signals.push({ factor: "priority", points: priorityPts });
19316
+ score += priorityPts;
19317
+ }
19318
+ if (["action", "question"].includes(doc.frontmatter.type)) {
19319
+ const createdDays = daysBetween(doc.frontmatter.created, today);
19320
+ if (createdDays >= 14) {
19321
+ const agingPts = Math.min(10, Math.floor((createdDays - 14) / 7) * 3 + 5);
19322
+ signals.push({ factor: "aging", points: agingPts });
19323
+ score += agingPts;
19324
+ }
19325
+ }
19326
+ const refs = crossRefCounts.get(doc.frontmatter.id) ?? 0;
19327
+ if (refs > 0) {
19328
+ const crossRefPts = Math.min(15, refs * 5);
19329
+ signals.push({ factor: "cross-references", points: crossRefPts });
19330
+ score += crossRefPts;
19331
+ }
19332
+ return {
19333
+ id: doc.frontmatter.id,
19334
+ title: doc.frontmatter.title,
19335
+ type: doc.frontmatter.type,
19336
+ status: doc.frontmatter.status,
19337
+ score,
19338
+ signals
19339
+ };
19340
+ }).filter((item) => item.score > 0).sort((a, b) => b.score - a.score).slice(0, 15);
19341
+ return { dueSoonActions, dueSoonSprintTasks, trending };
19342
+ }
19132
19343
 
19133
19344
  // src/web/templates/layout.ts
19134
19345
  function escapeHtml(str) {
@@ -19252,6 +19463,7 @@ function inline(text) {
19252
19463
  function layout(opts, body) {
19253
19464
  const topItems = [
19254
19465
  { href: "/", label: "Overview" },
19466
+ { href: "/upcoming", label: "Upcoming" },
19255
19467
  { href: "/timeline", label: "Timeline" },
19256
19468
  { href: "/board", label: "Board" },
19257
19469
  { href: "/gar", label: "GAR Report" },
@@ -20138,6 +20350,56 @@ tr:hover td {
20138
20350
  fill: var(--bg) !important;
20139
20351
  font-weight: 600;
20140
20352
  }
20353
+
20354
+ /* Urgency row indicators */
20355
+ .urgency-row-overdue { border-left: 3px solid var(--red); }
20356
+ .urgency-row-due-3d { border-left: 3px solid var(--amber); }
20357
+ .urgency-row-due-7d { border-left: 3px solid #e2a308; }
20358
+
20359
+ /* Urgency badge pills */
20360
+ .urgency-badge-overdue { background: rgba(248, 113, 113, 0.15); color: var(--red); }
20361
+ .urgency-badge-due-3d { background: rgba(251, 191, 36, 0.15); color: var(--amber); }
20362
+ .urgency-badge-due-7d { background: rgba(226, 163, 8, 0.15); color: #e2a308; }
20363
+ .urgency-badge-upcoming { background: rgba(108, 140, 255, 0.15); color: var(--accent); }
20364
+ .urgency-badge-later { background: rgba(139, 143, 164, 0.1); color: var(--text-dim); }
20365
+
20366
+ /* Trending */
20367
+ .trending-rank {
20368
+ display: inline-flex;
20369
+ align-items: center;
20370
+ justify-content: center;
20371
+ width: 24px;
20372
+ height: 24px;
20373
+ border-radius: 50%;
20374
+ background: var(--bg-hover);
20375
+ font-size: 0.75rem;
20376
+ font-weight: 600;
20377
+ color: var(--text-dim);
20378
+ }
20379
+
20380
+ .trending-score {
20381
+ display: inline-block;
20382
+ padding: 0.15rem 0.6rem;
20383
+ border-radius: 999px;
20384
+ font-size: 0.7rem;
20385
+ font-weight: 700;
20386
+ background: rgba(108, 140, 255, 0.15);
20387
+ color: var(--accent);
20388
+ }
20389
+
20390
+ .signal-tag {
20391
+ display: inline-block;
20392
+ padding: 0.1rem 0.45rem;
20393
+ border-radius: 4px;
20394
+ font-size: 0.65rem;
20395
+ background: var(--bg-hover);
20396
+ color: var(--text-dim);
20397
+ margin-right: 0.25rem;
20398
+ margin-bottom: 0.15rem;
20399
+ white-space: nowrap;
20400
+ }
20401
+
20402
+ .text-dim { color: var(--text-dim); }
20141
20403
  `;
20142
20404
  }
20143
20405
 
@@ -20310,13 +20572,14 @@ function buildArtifactFlowchart(data) {
20310
20572
  var svg = document.getElementById('flow-lines');
20311
20573
  if (!container || !svg) return;
20312
20574
 
20313
- // Build adjacency map (bidirectional) for traversal
20314
- var adj = {};
20575
+ // Build directed adjacency maps for traversal
20576
+ var fwd = {}; // from \u2192 [to] (Feature\u2192Epic, Epic\u2192Sprint)
20577
+ var bwd = {}; // to \u2192 [from] (Sprint\u2192Epic, Epic\u2192Feature)
20315
20578
  edges.forEach(function(e) {
20316
- if (!adj[e.from]) adj[e.from] = [];
20317
- if (!adj[e.to]) adj[e.to] = [];
20318
- adj[e.from].push(e.to);
20319
- adj[e.to].push(e.from);
20579
+ if (!fwd[e.from]) fwd[e.from] = [];
20580
+ if (!bwd[e.to]) bwd[e.to] = [];
20581
+ fwd[e.from].push(e.to);
20582
+ bwd[e.to].push(e.from);
20320
20583
  });
20321
20584
 
20322
20585
  function drawLines() {
@@ -20349,14 +20612,28 @@ function buildArtifactFlowchart(data) {
20349
20612
  });
20350
20613
  }
20351
20614
 
20352
- // Find all nodes reachable from a starting node
20615
+ // Find directly related nodes via directed traversal
20616
+ // Follows forward edges (Feature\u2192Epic\u2192Sprint) and backward edges
20617
+ // (Sprint\u2192Epic\u2192Feature) separately to avoid sideways expansion
20353
20618
  function findConnected(startId) {
20354
20619
  var visited = {};
20355
- var queue = [startId];
20356
20620
  visited[startId] = true;
20621
+ // Traverse forward (from\u2192to direction)
20622
+ var queue = [startId];
20357
20623
  while (queue.length) {
20358
20624
  var id = queue.shift();
20359
- (adj[id] || []).forEach(function(neighbor) {
20625
+ (fwd[id] || []).forEach(function(neighbor) {
20626
+ if (!visited[neighbor]) {
20627
+ visited[neighbor] = true;
20628
+ queue.push(neighbor);
20629
+ }
20630
+ });
20631
+ }
20632
+ // Traverse backward (to\u2192from direction)
20633
+ queue = [startId];
20634
+ while (queue.length) {
20635
+ var id = queue.shift();
20636
+ (bwd[id] || []).forEach(function(neighbor) {
20360
20637
  if (!visited[neighbor]) {
20361
20638
  visited[neighbor] = true;
20362
20639
  queue.push(neighbor);
@@ -20798,6 +21075,131 @@ function timelinePage(diagrams) {
20798
21075
  `;
20799
21076
  }
20800
21077
 
21078
+ // src/web/templates/pages/upcoming.ts
21079
+ function urgencyBadge(tier) {
21080
+ const labels = {
21081
+ overdue: "Overdue",
21082
+ "due-3d": "Due in 3d",
21083
+ "due-7d": "Due in 7d",
21084
+ upcoming: "Upcoming",
21085
+ later: "Later"
21086
+ };
21087
+ return `<span class="badge urgency-badge-${tier}">${labels[tier]}</span>`;
21088
+ }
21089
+ function urgencyRowClass(tier) {
21090
+ if (tier === "overdue") return " urgency-row-overdue";
21091
+ if (tier === "due-3d") return " urgency-row-due-3d";
21092
+ if (tier === "due-7d") return " urgency-row-due-7d";
21093
+ return "";
21094
+ }
21095
+ function upcomingPage(data) {
21096
+ const hasActions = data.dueSoonActions.length > 0;
21097
+ const hasSprintTasks = data.dueSoonSprintTasks.length > 0;
21098
+ const hasTrending = data.trending.length > 0;
21099
+ const actionsTable = hasActions ? `
21100
+ <h3 class="section-title">Due Soon \u2014 Actions</h3>
21101
+ <div class="table-wrap">
21102
+ <table>
21103
+ <thead>
21104
+ <tr>
21105
+ <th>ID</th>
21106
+ <th>Title</th>
21107
+ <th>Status</th>
21108
+ <th>Owner</th>
21109
+ <th>Due Date</th>
21110
+ <th>Urgency</th>
21111
+ <th>Tasks</th>
21112
+ </tr>
21113
+ </thead>
21114
+ <tbody>
21115
+ ${data.dueSoonActions.map(
21116
+ (a) => `
21117
+ <tr class="${urgencyRowClass(a.urgency)}">
21118
+ <td><a href="/docs/action/${escapeHtml(a.id)}">${escapeHtml(a.id)}</a></td>
21119
+ <td>${escapeHtml(a.title)}</td>
21120
+ <td>${statusBadge(a.status)}</td>
21121
+ <td>${a.owner ? escapeHtml(a.owner) : '<span class="text-dim">\u2014</span>'}</td>
21122
+ <td>${formatDate(a.dueDate)}</td>
21123
+ <td>${urgencyBadge(a.urgency)}</td>
21124
+ <td>${a.relatedTaskCount > 0 ? a.relatedTaskCount : "\u2014"}</td>
21125
+ </tr>`
21126
+ ).join("")}
21127
+ </tbody>
21128
+ </table>
21129
+ </div>` : "";
21130
+ const sprintTasksTable = hasSprintTasks ? `
21131
+ <h3 class="section-title">Due Soon \u2014 Sprint Tasks</h3>
21132
+ <div class="table-wrap">
21133
+ <table>
21134
+ <thead>
21135
+ <tr>
21136
+ <th>ID</th>
21137
+ <th>Title</th>
21138
+ <th>Status</th>
21139
+ <th>Sprint</th>
21140
+ <th>Sprint Ends</th>
21141
+ <th>Urgency</th>
21142
+ </tr>
21143
+ </thead>
21144
+ <tbody>
21145
+ ${data.dueSoonSprintTasks.map(
21146
+ (t) => `
21147
+ <tr class="${urgencyRowClass(t.urgency)}">
21148
+ <td><a href="/docs/task/${escapeHtml(t.id)}">${escapeHtml(t.id)}</a></td>
21149
+ <td>${escapeHtml(t.title)}</td>
21150
+ <td>${statusBadge(t.status)}</td>
21151
+ <td><a href="/docs/sprint/${escapeHtml(t.sprintId)}">${escapeHtml(t.sprintId)}</a></td>
21152
+ <td>${formatDate(t.sprintEndDate)}</td>
21153
+ <td>${urgencyBadge(t.urgency)}</td>
21154
+ </tr>`
21155
+ ).join("")}
21156
+ </tbody>
21157
+ </table>
21158
+ </div>` : "";
21159
+ const trendingTable = hasTrending ? `
21160
+ <h3 class="section-title">Trending</h3>
21161
+ <div class="table-wrap">
21162
+ <table>
21163
+ <thead>
21164
+ <tr>
21165
+ <th>#</th>
21166
+ <th>ID</th>
21167
+ <th>Title</th>
21168
+ <th>Type</th>
21169
+ <th>Status</th>
21170
+ <th>Score</th>
21171
+ <th>Signals</th>
21172
+ </tr>
21173
+ </thead>
21174
+ <tbody>
21175
+ ${data.trending.map(
21176
+ (t, i) => `
21177
+ <tr>
21178
+ <td><span class="trending-rank">${i + 1}</span></td>
21179
+ <td><a href="/docs/${escapeHtml(t.type)}/${escapeHtml(t.id)}">${escapeHtml(t.id)}</a></td>
21180
+ <td>${escapeHtml(t.title)}</td>
21181
+ <td>${escapeHtml(typeLabel(t.type))}</td>
21182
+ <td>${statusBadge(t.status)}</td>
21183
+ <td><span class="trending-score">${t.score}</span></td>
21184
+ <td>${t.signals.map((s) => `<span class="signal-tag">${escapeHtml(s.factor)} +${s.points}</span>`).join(" ")}</td>
21185
+ </tr>`
21186
+ ).join("")}
21187
+ </tbody>
21188
+ </table>
21189
+ </div>` : "";
21190
+ const emptyState = !hasActions && !hasSprintTasks && !hasTrending ? '<div class="empty"><p>No upcoming items or trending activity found.</p></div>' : "";
21191
+ return `
21192
+ <div class="page-header">
21193
+ <h2>Upcoming</h2>
21194
+ <div class="subtitle">Time-sensitive items and trending activity</div>
21195
+ </div>
21196
+ ${actionsTable}
21197
+ ${sprintTasksTable}
21198
+ ${trendingTable}
21199
+ ${emptyState}
21200
+ `;
21201
+ }
21202
+
20801
21203
  // src/web/router.ts
20802
21204
  function handleRequest(req, res, store, projectName, navGroups) {
20803
21205
  const parsed = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
@@ -20838,6 +21240,12 @@ function handleRequest(req, res, store, projectName, navGroups) {
20838
21240
  respond(res, layout({ title: "Health Check", activePath: "/health", projectName, navGroups }, body));
20839
21241
  return;
20840
21242
  }
21243
+ if (pathname === "/upcoming") {
21244
+ const data = getUpcomingData(store);
21245
+ const body = upcomingPage(data);
21246
+ respond(res, layout({ title: "Upcoming", activePath: "/upcoming", projectName, navGroups }, body));
21247
+ return;
21248
+ }
20841
21249
  const boardMatch = pathname.match(/^\/board(?:\/([^/]+))?$/);
20842
21250
  if (boardMatch) {
20843
21251
  const type = boardMatch[1];
@@ -20995,6 +21403,7 @@ function createWebTools(store, projectName, navGroups) {
20995
21403
  const base = `http://localhost:${runningServer.port}`;
20996
21404
  const urls = {
20997
21405
  overview: base,
21406
+ upcoming: `${base}/upcoming`,
20998
21407
  gar: `${base}/gar`,
20999
21408
  board: `${base}/board`
21000
21409
  };
@@ -21068,6 +21477,18 @@ function createWebTools(store, projectName, navGroups) {
21068
21477
  };
21069
21478
  },
21070
21479
  { annotations: { readOnlyHint: true } }
21480
+ ),
21481
+ tool22(
21482
+ "get_dashboard_upcoming",
21483
+ "Get upcoming data: due-soon actions and sprint tasks, plus trending items scored by relevance signals. Works without the web server running.",
21484
+ {},
21485
+ async () => {
21486
+ const data = getUpcomingData(store);
21487
+ return {
21488
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
21489
+ };
21490
+ },
21491
+ { annotations: { readOnlyHint: true } }
21071
21492
  )
21072
21493
  ];
21073
21494
  }
@@ -21527,7 +21948,7 @@ function collectTools(marvinDir) {
21527
21948
  const sessionStore = new SessionStore(marvinDir);
21528
21949
  const allSkills = loadAllSkills(marvinDir);
21529
21950
  const allSkillIds = [...allSkills.keys()];
21530
- const codeSkillTools = getSkillTools(allSkillIds, allSkills, store);
21951
+ const codeSkillTools = getSkillTools(allSkillIds, allSkills, store, config2);
21531
21952
  const skillsWithActions = allSkillIds.map((id) => allSkills.get(id)).filter((s) => s.actions && s.actions.length > 0);
21532
21953
  const projectRoot = path9.dirname(marvinDir);
21533
21954
  const actionTools = createSkillActionTools(skillsWithActions, { store, marvinDir, projectRoot });