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-serve.js
CHANGED
|
@@ -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:
|
|
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
|
|
20314
|
-
var
|
|
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 (!
|
|
20317
|
-
if (!
|
|
20318
|
-
|
|
20319
|
-
|
|
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
|
|
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
|
-
(
|
|
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 });
|