mrvn-cli 0.4.4 → 0.4.6
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 +844 -242
- package/dist/index.js.map +1 -1
- package/dist/marvin-serve.js +697 -94
- package/dist/marvin-serve.js.map +1 -1
- package/dist/marvin.js +700 -96
- package/dist/marvin.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -15019,132 +15019,22 @@ function generateFeatureTags(features) {
|
|
|
15019
15019
|
return features.map((id) => `feature:${id}`);
|
|
15020
15020
|
}
|
|
15021
15021
|
|
|
15022
|
-
// src/
|
|
15023
|
-
function
|
|
15024
|
-
|
|
15025
|
-
|
|
15026
|
-
|
|
15027
|
-
|
|
15028
|
-
|
|
15029
|
-
|
|
15030
|
-
);
|
|
15031
|
-
const tagOverdueItems = allDocs.filter(
|
|
15032
|
-
(d) => d.frontmatter.tags?.includes("overdue")
|
|
15033
|
-
);
|
|
15034
|
-
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
15035
|
-
const dateOverdueActions = openActions.filter((d) => {
|
|
15036
|
-
const dueDate = d.frontmatter.dueDate;
|
|
15037
|
-
return typeof dueDate === "string" && dueDate < today;
|
|
15038
|
-
});
|
|
15039
|
-
const overdueItems = [...tagOverdueItems, ...dateOverdueActions].filter(
|
|
15040
|
-
(d, i, arr) => arr.findIndex((x) => x.frontmatter.id === d.frontmatter.id) === i
|
|
15041
|
-
);
|
|
15042
|
-
const openQuestions = store.list({ type: "question", status: "open" });
|
|
15043
|
-
const riskItems = allDocs.filter(
|
|
15044
|
-
(d) => d.frontmatter.tags?.includes("risk") && d.frontmatter.status !== "done" && d.frontmatter.status !== "closed"
|
|
15045
|
-
);
|
|
15046
|
-
const unownedActions = openActions.filter((d) => !d.frontmatter.owner);
|
|
15047
|
-
const total = allActions.length;
|
|
15048
|
-
const done = doneActions.length;
|
|
15049
|
-
const completionPct = total > 0 ? Math.round(done / total * 100) : 100;
|
|
15050
|
-
const scheduleItems = [
|
|
15051
|
-
...blockedItems,
|
|
15052
|
-
...overdueItems
|
|
15053
|
-
].filter(
|
|
15054
|
-
(d, i, arr) => arr.findIndex((x) => x.frontmatter.id === d.frontmatter.id) === i
|
|
15055
|
-
).map((d) => ({ id: d.frontmatter.id, title: d.frontmatter.title }));
|
|
15056
|
-
const qualityItems = [
|
|
15057
|
-
...riskItems,
|
|
15058
|
-
...openQuestions
|
|
15059
|
-
].filter(
|
|
15060
|
-
(d, i, arr) => arr.findIndex((x) => x.frontmatter.id === d.frontmatter.id) === i
|
|
15061
|
-
).map((d) => ({ id: d.frontmatter.id, title: d.frontmatter.title }));
|
|
15062
|
-
const resourceItems = unownedActions.map((d) => ({
|
|
15063
|
-
id: d.frontmatter.id,
|
|
15064
|
-
title: d.frontmatter.title
|
|
15065
|
-
}));
|
|
15066
|
-
return {
|
|
15067
|
-
scope: {
|
|
15068
|
-
total,
|
|
15069
|
-
open: openActions.length,
|
|
15070
|
-
done,
|
|
15071
|
-
completionPct
|
|
15072
|
-
},
|
|
15073
|
-
schedule: {
|
|
15074
|
-
blocked: blockedItems.length,
|
|
15075
|
-
overdue: overdueItems.length,
|
|
15076
|
-
items: scheduleItems
|
|
15077
|
-
},
|
|
15078
|
-
quality: {
|
|
15079
|
-
risks: riskItems.length,
|
|
15080
|
-
openQuestions: openQuestions.length,
|
|
15081
|
-
items: qualityItems
|
|
15082
|
-
},
|
|
15083
|
-
resources: {
|
|
15084
|
-
unowned: unownedActions.length,
|
|
15085
|
-
items: resourceItems
|
|
15022
|
+
// src/plugins/builtin/tools/task-utils.ts
|
|
15023
|
+
function normalizeLinkedEpics(value) {
|
|
15024
|
+
if (value === void 0 || value === null) return [];
|
|
15025
|
+
if (typeof value === "string") {
|
|
15026
|
+
try {
|
|
15027
|
+
const parsed = JSON.parse(value);
|
|
15028
|
+
if (Array.isArray(parsed)) return parsed.filter((v) => typeof v === "string");
|
|
15029
|
+
} catch {
|
|
15086
15030
|
}
|
|
15087
|
-
|
|
15088
|
-
}
|
|
15089
|
-
|
|
15090
|
-
|
|
15091
|
-
function worstStatus(statuses) {
|
|
15092
|
-
if (statuses.includes("red")) return "red";
|
|
15093
|
-
if (statuses.includes("amber")) return "amber";
|
|
15094
|
-
return "green";
|
|
15031
|
+
return [value];
|
|
15032
|
+
}
|
|
15033
|
+
if (Array.isArray(value)) return value.filter((v) => typeof v === "string");
|
|
15034
|
+
return [];
|
|
15095
15035
|
}
|
|
15096
|
-
function
|
|
15097
|
-
|
|
15098
|
-
const scopePct = metrics.scope.completionPct;
|
|
15099
|
-
const scopeStatus = scopePct >= 70 ? "green" : scopePct >= 40 ? "amber" : "red";
|
|
15100
|
-
areas.push({
|
|
15101
|
-
name: "Scope",
|
|
15102
|
-
status: scopeStatus,
|
|
15103
|
-
summary: `${scopePct}% complete (${metrics.scope.done}/${metrics.scope.total})`,
|
|
15104
|
-
items: []
|
|
15105
|
-
});
|
|
15106
|
-
const scheduleCount = metrics.schedule.blocked + metrics.schedule.overdue;
|
|
15107
|
-
const scheduleStatus = scheduleCount === 0 ? "green" : scheduleCount <= 2 ? "amber" : "red";
|
|
15108
|
-
const scheduleParts = [];
|
|
15109
|
-
if (metrics.schedule.blocked > 0)
|
|
15110
|
-
scheduleParts.push(`${metrics.schedule.blocked} blocked`);
|
|
15111
|
-
if (metrics.schedule.overdue > 0)
|
|
15112
|
-
scheduleParts.push(`${metrics.schedule.overdue} overdue`);
|
|
15113
|
-
areas.push({
|
|
15114
|
-
name: "Schedule",
|
|
15115
|
-
status: scheduleStatus,
|
|
15116
|
-
summary: scheduleParts.length > 0 ? scheduleParts.join(", ") : "on track",
|
|
15117
|
-
items: metrics.schedule.items
|
|
15118
|
-
});
|
|
15119
|
-
const qualityCount = metrics.quality.risks + metrics.quality.openQuestions;
|
|
15120
|
-
const qualityStatus = qualityCount === 0 ? "green" : qualityCount <= 2 ? "amber" : "red";
|
|
15121
|
-
const qualityParts = [];
|
|
15122
|
-
if (metrics.quality.risks > 0)
|
|
15123
|
-
qualityParts.push(`${metrics.quality.risks} risk(s)`);
|
|
15124
|
-
if (metrics.quality.openQuestions > 0)
|
|
15125
|
-
qualityParts.push(`${metrics.quality.openQuestions} open question(s)`);
|
|
15126
|
-
areas.push({
|
|
15127
|
-
name: "Quality",
|
|
15128
|
-
status: qualityStatus,
|
|
15129
|
-
summary: qualityParts.length > 0 ? qualityParts.join(", ") : "no issues",
|
|
15130
|
-
items: metrics.quality.items
|
|
15131
|
-
});
|
|
15132
|
-
const resourceCount = metrics.resources.unowned;
|
|
15133
|
-
const resourceStatus = resourceCount === 0 ? "green" : resourceCount <= 2 ? "amber" : "red";
|
|
15134
|
-
areas.push({
|
|
15135
|
-
name: "Resources",
|
|
15136
|
-
status: resourceStatus,
|
|
15137
|
-
summary: resourceCount > 0 ? `${resourceCount} unowned action(s)` : "all assigned",
|
|
15138
|
-
items: metrics.resources.items
|
|
15139
|
-
});
|
|
15140
|
-
const overall = worstStatus(areas.map((a) => a.status));
|
|
15141
|
-
return {
|
|
15142
|
-
projectName,
|
|
15143
|
-
generatedAt: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
15144
|
-
overall,
|
|
15145
|
-
areas,
|
|
15146
|
-
metrics
|
|
15147
|
-
};
|
|
15036
|
+
function generateEpicTags(epics) {
|
|
15037
|
+
return epics.map((id) => `epic:${id}`);
|
|
15148
15038
|
}
|
|
15149
15039
|
|
|
15150
15040
|
// src/reports/health/collector.ts
|
|
@@ -15281,8 +15171,136 @@ function collectProcess(store) {
|
|
|
15281
15171
|
}
|
|
15282
15172
|
function collectHealthMetrics(store) {
|
|
15283
15173
|
return {
|
|
15284
|
-
completeness: collectCompleteness(store),
|
|
15285
|
-
process: collectProcess(store)
|
|
15174
|
+
completeness: collectCompleteness(store),
|
|
15175
|
+
process: collectProcess(store)
|
|
15176
|
+
};
|
|
15177
|
+
}
|
|
15178
|
+
|
|
15179
|
+
// src/reports/gar/collector.ts
|
|
15180
|
+
function collectGarMetrics(store) {
|
|
15181
|
+
const allActions = store.list({ type: "action" });
|
|
15182
|
+
const openActions = allActions.filter((d) => d.frontmatter.status === "open");
|
|
15183
|
+
const doneActions = allActions.filter((d) => d.frontmatter.status === "done");
|
|
15184
|
+
const allDocs = store.list();
|
|
15185
|
+
const blockedItems = allDocs.filter(
|
|
15186
|
+
(d) => d.frontmatter.tags?.includes("blocked")
|
|
15187
|
+
);
|
|
15188
|
+
const tagOverdueItems = allDocs.filter(
|
|
15189
|
+
(d) => d.frontmatter.tags?.includes("overdue")
|
|
15190
|
+
);
|
|
15191
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
15192
|
+
const dateOverdueActions = openActions.filter((d) => {
|
|
15193
|
+
const dueDate = d.frontmatter.dueDate;
|
|
15194
|
+
return typeof dueDate === "string" && dueDate < today;
|
|
15195
|
+
});
|
|
15196
|
+
const overdueItems = [...tagOverdueItems, ...dateOverdueActions].filter(
|
|
15197
|
+
(d, i, arr) => arr.findIndex((x) => x.frontmatter.id === d.frontmatter.id) === i
|
|
15198
|
+
);
|
|
15199
|
+
const openQuestions = store.list({ type: "question", status: "open" });
|
|
15200
|
+
const riskItems = allDocs.filter(
|
|
15201
|
+
(d) => d.frontmatter.tags?.includes("risk") && d.frontmatter.status !== "done" && d.frontmatter.status !== "closed"
|
|
15202
|
+
);
|
|
15203
|
+
const unownedActions = openActions.filter((d) => !d.frontmatter.owner);
|
|
15204
|
+
const total = allActions.length;
|
|
15205
|
+
const done = doneActions.length;
|
|
15206
|
+
const completionPct = total > 0 ? Math.round(done / total * 100) : 100;
|
|
15207
|
+
const scheduleItems = [
|
|
15208
|
+
...blockedItems,
|
|
15209
|
+
...overdueItems
|
|
15210
|
+
].filter(
|
|
15211
|
+
(d, i, arr) => arr.findIndex((x) => x.frontmatter.id === d.frontmatter.id) === i
|
|
15212
|
+
).map((d) => ({ id: d.frontmatter.id, title: d.frontmatter.title }));
|
|
15213
|
+
const qualityItems = [
|
|
15214
|
+
...riskItems,
|
|
15215
|
+
...openQuestions
|
|
15216
|
+
].filter(
|
|
15217
|
+
(d, i, arr) => arr.findIndex((x) => x.frontmatter.id === d.frontmatter.id) === i
|
|
15218
|
+
).map((d) => ({ id: d.frontmatter.id, title: d.frontmatter.title }));
|
|
15219
|
+
const resourceItems = unownedActions.map((d) => ({
|
|
15220
|
+
id: d.frontmatter.id,
|
|
15221
|
+
title: d.frontmatter.title
|
|
15222
|
+
}));
|
|
15223
|
+
return {
|
|
15224
|
+
scope: {
|
|
15225
|
+
total,
|
|
15226
|
+
open: openActions.length,
|
|
15227
|
+
done,
|
|
15228
|
+
completionPct
|
|
15229
|
+
},
|
|
15230
|
+
schedule: {
|
|
15231
|
+
blocked: blockedItems.length,
|
|
15232
|
+
overdue: overdueItems.length,
|
|
15233
|
+
items: scheduleItems
|
|
15234
|
+
},
|
|
15235
|
+
quality: {
|
|
15236
|
+
risks: riskItems.length,
|
|
15237
|
+
openQuestions: openQuestions.length,
|
|
15238
|
+
items: qualityItems
|
|
15239
|
+
},
|
|
15240
|
+
resources: {
|
|
15241
|
+
unowned: unownedActions.length,
|
|
15242
|
+
items: resourceItems
|
|
15243
|
+
}
|
|
15244
|
+
};
|
|
15245
|
+
}
|
|
15246
|
+
|
|
15247
|
+
// src/reports/gar/evaluator.ts
|
|
15248
|
+
function worstStatus(statuses) {
|
|
15249
|
+
if (statuses.includes("red")) return "red";
|
|
15250
|
+
if (statuses.includes("amber")) return "amber";
|
|
15251
|
+
return "green";
|
|
15252
|
+
}
|
|
15253
|
+
function evaluateGar(projectName, metrics) {
|
|
15254
|
+
const areas = [];
|
|
15255
|
+
const scopePct = metrics.scope.completionPct;
|
|
15256
|
+
const scopeStatus = scopePct >= 70 ? "green" : scopePct >= 40 ? "amber" : "red";
|
|
15257
|
+
areas.push({
|
|
15258
|
+
name: "Scope",
|
|
15259
|
+
status: scopeStatus,
|
|
15260
|
+
summary: `${scopePct}% complete (${metrics.scope.done}/${metrics.scope.total})`,
|
|
15261
|
+
items: []
|
|
15262
|
+
});
|
|
15263
|
+
const scheduleCount = metrics.schedule.blocked + metrics.schedule.overdue;
|
|
15264
|
+
const scheduleStatus = scheduleCount === 0 ? "green" : scheduleCount <= 2 ? "amber" : "red";
|
|
15265
|
+
const scheduleParts = [];
|
|
15266
|
+
if (metrics.schedule.blocked > 0)
|
|
15267
|
+
scheduleParts.push(`${metrics.schedule.blocked} blocked`);
|
|
15268
|
+
if (metrics.schedule.overdue > 0)
|
|
15269
|
+
scheduleParts.push(`${metrics.schedule.overdue} overdue`);
|
|
15270
|
+
areas.push({
|
|
15271
|
+
name: "Schedule",
|
|
15272
|
+
status: scheduleStatus,
|
|
15273
|
+
summary: scheduleParts.length > 0 ? scheduleParts.join(", ") : "on track",
|
|
15274
|
+
items: metrics.schedule.items
|
|
15275
|
+
});
|
|
15276
|
+
const qualityCount = metrics.quality.risks + metrics.quality.openQuestions;
|
|
15277
|
+
const qualityStatus = qualityCount === 0 ? "green" : qualityCount <= 2 ? "amber" : "red";
|
|
15278
|
+
const qualityParts = [];
|
|
15279
|
+
if (metrics.quality.risks > 0)
|
|
15280
|
+
qualityParts.push(`${metrics.quality.risks} risk(s)`);
|
|
15281
|
+
if (metrics.quality.openQuestions > 0)
|
|
15282
|
+
qualityParts.push(`${metrics.quality.openQuestions} open question(s)`);
|
|
15283
|
+
areas.push({
|
|
15284
|
+
name: "Quality",
|
|
15285
|
+
status: qualityStatus,
|
|
15286
|
+
summary: qualityParts.length > 0 ? qualityParts.join(", ") : "no issues",
|
|
15287
|
+
items: metrics.quality.items
|
|
15288
|
+
});
|
|
15289
|
+
const resourceCount = metrics.resources.unowned;
|
|
15290
|
+
const resourceStatus = resourceCount === 0 ? "green" : resourceCount <= 2 ? "amber" : "red";
|
|
15291
|
+
areas.push({
|
|
15292
|
+
name: "Resources",
|
|
15293
|
+
status: resourceStatus,
|
|
15294
|
+
summary: resourceCount > 0 ? `${resourceCount} unowned action(s)` : "all assigned",
|
|
15295
|
+
items: metrics.resources.items
|
|
15296
|
+
});
|
|
15297
|
+
const overall = worstStatus(areas.map((a) => a.status));
|
|
15298
|
+
return {
|
|
15299
|
+
projectName,
|
|
15300
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
15301
|
+
overall,
|
|
15302
|
+
areas,
|
|
15303
|
+
metrics
|
|
15286
15304
|
};
|
|
15287
15305
|
}
|
|
15288
15306
|
|
|
@@ -15505,8 +15523,223 @@ function getDiagramData(store) {
|
|
|
15505
15523
|
}
|
|
15506
15524
|
return { sprints, epics, features, statusCounts };
|
|
15507
15525
|
}
|
|
15526
|
+
function computeUrgency(dueDateStr, todayStr) {
|
|
15527
|
+
const due = new Date(dueDateStr).getTime();
|
|
15528
|
+
const today = new Date(todayStr).getTime();
|
|
15529
|
+
const diffDays = Math.floor((due - today) / 864e5);
|
|
15530
|
+
if (diffDays < 0) return "overdue";
|
|
15531
|
+
if (diffDays <= 3) return "due-3d";
|
|
15532
|
+
if (diffDays <= 7) return "due-7d";
|
|
15533
|
+
if (diffDays <= 14) return "upcoming";
|
|
15534
|
+
return "later";
|
|
15535
|
+
}
|
|
15536
|
+
var DONE_STATUSES = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
|
|
15537
|
+
function getUpcomingData(store) {
|
|
15538
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
15539
|
+
const allDocs = store.list();
|
|
15540
|
+
const docById = /* @__PURE__ */ new Map();
|
|
15541
|
+
for (const doc of allDocs) {
|
|
15542
|
+
docById.set(doc.frontmatter.id, doc);
|
|
15543
|
+
}
|
|
15544
|
+
const actions = allDocs.filter(
|
|
15545
|
+
(d) => d.frontmatter.type === "action" && !DONE_STATUSES.has(d.frontmatter.status)
|
|
15546
|
+
);
|
|
15547
|
+
const actionsWithDue = actions.filter((d) => d.frontmatter.dueDate);
|
|
15548
|
+
const sprints = allDocs.filter((d) => d.frontmatter.type === "sprint");
|
|
15549
|
+
const epics = allDocs.filter((d) => d.frontmatter.type === "epic");
|
|
15550
|
+
const tasks = allDocs.filter((d) => d.frontmatter.type === "task");
|
|
15551
|
+
const epicToTasks = /* @__PURE__ */ new Map();
|
|
15552
|
+
for (const task of tasks) {
|
|
15553
|
+
const tags = task.frontmatter.tags ?? [];
|
|
15554
|
+
for (const tag of tags) {
|
|
15555
|
+
if (tag.startsWith("epic:")) {
|
|
15556
|
+
const epicId = tag.slice(5);
|
|
15557
|
+
if (!epicToTasks.has(epicId)) epicToTasks.set(epicId, []);
|
|
15558
|
+
epicToTasks.get(epicId).push(task);
|
|
15559
|
+
}
|
|
15560
|
+
}
|
|
15561
|
+
}
|
|
15562
|
+
function getSprintTasks(sprintDoc) {
|
|
15563
|
+
const linkedEpics = normalizeLinkedEpics(sprintDoc.frontmatter.linkedEpics);
|
|
15564
|
+
const result = [];
|
|
15565
|
+
for (const epicId of linkedEpics) {
|
|
15566
|
+
const epicTasks = epicToTasks.get(epicId) ?? [];
|
|
15567
|
+
result.push(...epicTasks);
|
|
15568
|
+
}
|
|
15569
|
+
return result;
|
|
15570
|
+
}
|
|
15571
|
+
function countRelatedTasks(actionDoc) {
|
|
15572
|
+
const actionTags = actionDoc.frontmatter.tags ?? [];
|
|
15573
|
+
const relatedTaskIds = /* @__PURE__ */ new Set();
|
|
15574
|
+
for (const tag of actionTags) {
|
|
15575
|
+
if (tag.startsWith("sprint:")) {
|
|
15576
|
+
const sprintId = tag.slice(7);
|
|
15577
|
+
const sprint = docById.get(sprintId);
|
|
15578
|
+
if (sprint) {
|
|
15579
|
+
const sprintTaskDocs = getSprintTasks(sprint);
|
|
15580
|
+
for (const t of sprintTaskDocs) relatedTaskIds.add(t.frontmatter.id);
|
|
15581
|
+
}
|
|
15582
|
+
}
|
|
15583
|
+
}
|
|
15584
|
+
return relatedTaskIds.size;
|
|
15585
|
+
}
|
|
15586
|
+
const dueSoonActions = actionsWithDue.map((d) => ({
|
|
15587
|
+
id: d.frontmatter.id,
|
|
15588
|
+
title: d.frontmatter.title,
|
|
15589
|
+
status: d.frontmatter.status,
|
|
15590
|
+
owner: d.frontmatter.owner,
|
|
15591
|
+
dueDate: d.frontmatter.dueDate,
|
|
15592
|
+
urgency: computeUrgency(d.frontmatter.dueDate, today),
|
|
15593
|
+
relatedTaskCount: countRelatedTasks(d)
|
|
15594
|
+
})).sort((a, b) => a.dueDate.localeCompare(b.dueDate));
|
|
15595
|
+
const todayMs = new Date(today).getTime();
|
|
15596
|
+
const fourteenDaysMs = 14 * 864e5;
|
|
15597
|
+
const nearSprints = sprints.filter((s) => {
|
|
15598
|
+
const endDate = s.frontmatter.endDate;
|
|
15599
|
+
if (!endDate) return false;
|
|
15600
|
+
const endMs = new Date(endDate).getTime();
|
|
15601
|
+
const diff = endMs - todayMs;
|
|
15602
|
+
return diff >= 0 && diff <= fourteenDaysMs;
|
|
15603
|
+
});
|
|
15604
|
+
const taskSprintMap = /* @__PURE__ */ new Map();
|
|
15605
|
+
for (const sprint of nearSprints) {
|
|
15606
|
+
const sprintEnd = sprint.frontmatter.endDate;
|
|
15607
|
+
const sprintTaskDocs = getSprintTasks(sprint);
|
|
15608
|
+
for (const task of sprintTaskDocs) {
|
|
15609
|
+
if (DONE_STATUSES.has(task.frontmatter.status)) continue;
|
|
15610
|
+
const existing = taskSprintMap.get(task.frontmatter.id);
|
|
15611
|
+
if (!existing || sprintEnd < existing.sprintEnd) {
|
|
15612
|
+
taskSprintMap.set(task.frontmatter.id, { task, sprint, sprintEnd });
|
|
15613
|
+
}
|
|
15614
|
+
}
|
|
15615
|
+
}
|
|
15616
|
+
const dueSoonSprintTasks = [...taskSprintMap.values()].map(({ task, sprint, sprintEnd }) => ({
|
|
15617
|
+
id: task.frontmatter.id,
|
|
15618
|
+
title: task.frontmatter.title,
|
|
15619
|
+
status: task.frontmatter.status,
|
|
15620
|
+
sprintId: sprint.frontmatter.id,
|
|
15621
|
+
sprintTitle: sprint.frontmatter.title,
|
|
15622
|
+
sprintEndDate: sprintEnd,
|
|
15623
|
+
urgency: computeUrgency(sprintEnd, today)
|
|
15624
|
+
})).sort((a, b) => a.sprintEndDate.localeCompare(b.sprintEndDate));
|
|
15625
|
+
const openItems = allDocs.filter(
|
|
15626
|
+
(d) => ["action", "question", "task"].includes(d.frontmatter.type) && !DONE_STATUSES.has(d.frontmatter.status)
|
|
15627
|
+
);
|
|
15628
|
+
const fourteenDaysAgo = new Date(todayMs - fourteenDaysMs).toISOString().slice(0, 10);
|
|
15629
|
+
const recentMeetings = allDocs.filter(
|
|
15630
|
+
(d) => d.frontmatter.type === "meeting" && (d.frontmatter.updated ?? d.frontmatter.created) >= fourteenDaysAgo
|
|
15631
|
+
);
|
|
15632
|
+
const crossRefCounts = /* @__PURE__ */ new Map();
|
|
15633
|
+
for (const doc of allDocs) {
|
|
15634
|
+
const content = doc.content ?? "";
|
|
15635
|
+
for (const item of openItems) {
|
|
15636
|
+
if (doc.frontmatter.id === item.frontmatter.id) continue;
|
|
15637
|
+
if (content.includes(item.frontmatter.id)) {
|
|
15638
|
+
crossRefCounts.set(
|
|
15639
|
+
item.frontmatter.id,
|
|
15640
|
+
(crossRefCounts.get(item.frontmatter.id) ?? 0) + 1
|
|
15641
|
+
);
|
|
15642
|
+
}
|
|
15643
|
+
}
|
|
15644
|
+
}
|
|
15645
|
+
const activeSprints = sprints.filter((s) => {
|
|
15646
|
+
const status = s.frontmatter.status;
|
|
15647
|
+
if (status === "active") return true;
|
|
15648
|
+
const startDate = s.frontmatter.startDate;
|
|
15649
|
+
if (!startDate) return false;
|
|
15650
|
+
const startMs = new Date(startDate).getTime();
|
|
15651
|
+
const diff = startMs - todayMs;
|
|
15652
|
+
return diff >= 0 && diff <= fourteenDaysMs;
|
|
15653
|
+
});
|
|
15654
|
+
const activeSprintIds = new Set(activeSprints.map((s) => s.frontmatter.id));
|
|
15655
|
+
const activeEpicIds = /* @__PURE__ */ new Set();
|
|
15656
|
+
for (const s of activeSprints) {
|
|
15657
|
+
for (const epicId of normalizeLinkedEpics(s.frontmatter.linkedEpics)) {
|
|
15658
|
+
activeEpicIds.add(epicId);
|
|
15659
|
+
}
|
|
15660
|
+
}
|
|
15661
|
+
const trending = openItems.map((doc) => {
|
|
15662
|
+
const signals = [];
|
|
15663
|
+
let score = 0;
|
|
15664
|
+
const updated = doc.frontmatter.updated ?? doc.frontmatter.created;
|
|
15665
|
+
const ageDays = daysBetween(updated, today);
|
|
15666
|
+
const recencyPts = Math.max(0, Math.round(20 * (1 - ageDays / 30)));
|
|
15667
|
+
if (recencyPts > 0) {
|
|
15668
|
+
signals.push({ factor: "recency", points: recencyPts });
|
|
15669
|
+
score += recencyPts;
|
|
15670
|
+
}
|
|
15671
|
+
const tags = doc.frontmatter.tags ?? [];
|
|
15672
|
+
const linkedToActiveSprint = tags.some(
|
|
15673
|
+
(t) => t.startsWith("sprint:") && activeSprintIds.has(t.slice(7))
|
|
15674
|
+
);
|
|
15675
|
+
const linkedToActiveEpic = tags.some(
|
|
15676
|
+
(t) => t.startsWith("epic:") && activeEpicIds.has(t.slice(5))
|
|
15677
|
+
);
|
|
15678
|
+
if (linkedToActiveSprint) {
|
|
15679
|
+
signals.push({ factor: "sprint proximity", points: 25 });
|
|
15680
|
+
score += 25;
|
|
15681
|
+
} else if (linkedToActiveEpic) {
|
|
15682
|
+
signals.push({ factor: "sprint proximity", points: 15 });
|
|
15683
|
+
score += 15;
|
|
15684
|
+
}
|
|
15685
|
+
const mentionCount = recentMeetings.filter(
|
|
15686
|
+
(m) => (m.content ?? "").includes(doc.frontmatter.id)
|
|
15687
|
+
).length;
|
|
15688
|
+
if (mentionCount > 0) {
|
|
15689
|
+
const meetingPts = Math.min(15, mentionCount * 5);
|
|
15690
|
+
signals.push({ factor: "meeting mentions", points: meetingPts });
|
|
15691
|
+
score += meetingPts;
|
|
15692
|
+
}
|
|
15693
|
+
const priority = doc.frontmatter.priority?.toLowerCase();
|
|
15694
|
+
const priorityPts = priority === "critical" ? 15 : priority === "high" ? 10 : priority === "medium" ? 3 : 0;
|
|
15695
|
+
if (priorityPts > 0) {
|
|
15696
|
+
signals.push({ factor: "priority", points: priorityPts });
|
|
15697
|
+
score += priorityPts;
|
|
15698
|
+
}
|
|
15699
|
+
if (["action", "question"].includes(doc.frontmatter.type)) {
|
|
15700
|
+
const createdDays = daysBetween(doc.frontmatter.created, today);
|
|
15701
|
+
if (createdDays >= 14) {
|
|
15702
|
+
const agingPts = Math.min(10, Math.floor((createdDays - 14) / 7) * 3 + 5);
|
|
15703
|
+
signals.push({ factor: "aging", points: agingPts });
|
|
15704
|
+
score += agingPts;
|
|
15705
|
+
}
|
|
15706
|
+
}
|
|
15707
|
+
const refs = crossRefCounts.get(doc.frontmatter.id) ?? 0;
|
|
15708
|
+
if (refs > 0) {
|
|
15709
|
+
const crossRefPts = Math.min(15, refs * 5);
|
|
15710
|
+
signals.push({ factor: "cross-references", points: crossRefPts });
|
|
15711
|
+
score += crossRefPts;
|
|
15712
|
+
}
|
|
15713
|
+
return {
|
|
15714
|
+
id: doc.frontmatter.id,
|
|
15715
|
+
title: doc.frontmatter.title,
|
|
15716
|
+
type: doc.frontmatter.type,
|
|
15717
|
+
status: doc.frontmatter.status,
|
|
15718
|
+
score,
|
|
15719
|
+
signals
|
|
15720
|
+
};
|
|
15721
|
+
}).filter((item) => item.score > 0).sort((a, b) => b.score - a.score).slice(0, 15);
|
|
15722
|
+
return { dueSoonActions, dueSoonSprintTasks, trending };
|
|
15723
|
+
}
|
|
15508
15724
|
|
|
15509
15725
|
// src/web/templates/layout.ts
|
|
15726
|
+
function collapsibleSection(sectionId, title, content, opts) {
|
|
15727
|
+
const tag = opts?.titleTag ?? "div";
|
|
15728
|
+
const cls = opts?.titleClass ?? "section-title";
|
|
15729
|
+
const collapsed = opts?.defaultCollapsed ? " collapsed" : "";
|
|
15730
|
+
return `
|
|
15731
|
+
<div class="collapsible${collapsed}" data-section-id="${escapeHtml(sectionId)}">
|
|
15732
|
+
<${tag} class="${cls} collapsible-header" onclick="toggleSection(this)">
|
|
15733
|
+
<svg class="collapsible-chevron" viewBox="0 0 16 16" width="14" height="14" fill="currentColor">
|
|
15734
|
+
<path d="M4.94 5.72a.75.75 0 0 1 1.06-.02L8 7.56l1.97-1.84a.75.75 0 1 1 1.02 1.1l-2.5 2.34a.75.75 0 0 1-1.02 0l-2.5-2.34a.75.75 0 0 1-.03-1.06z"/>
|
|
15735
|
+
</svg>
|
|
15736
|
+
<span>${title}</span>
|
|
15737
|
+
</${tag}>
|
|
15738
|
+
<div class="collapsible-body">
|
|
15739
|
+
${content}
|
|
15740
|
+
</div>
|
|
15741
|
+
</div>`;
|
|
15742
|
+
}
|
|
15510
15743
|
function escapeHtml(str) {
|
|
15511
15744
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
15512
15745
|
}
|
|
@@ -15628,6 +15861,7 @@ function inline(text) {
|
|
|
15628
15861
|
function layout(opts, body) {
|
|
15629
15862
|
const topItems = [
|
|
15630
15863
|
{ href: "/", label: "Overview" },
|
|
15864
|
+
{ href: "/upcoming", label: "Upcoming" },
|
|
15631
15865
|
{ href: "/timeline", label: "Timeline" },
|
|
15632
15866
|
{ href: "/board", label: "Board" },
|
|
15633
15867
|
{ href: "/gar", label: "GAR Report" },
|
|
@@ -15673,6 +15907,32 @@ function layout(opts, body) {
|
|
|
15673
15907
|
${body}
|
|
15674
15908
|
</main>
|
|
15675
15909
|
</div>
|
|
15910
|
+
<script>
|
|
15911
|
+
function toggleSection(header) {
|
|
15912
|
+
var section = header.closest('.collapsible');
|
|
15913
|
+
if (!section) return;
|
|
15914
|
+
section.classList.toggle('collapsed');
|
|
15915
|
+
var id = section.getAttribute('data-section-id');
|
|
15916
|
+
if (id) {
|
|
15917
|
+
try {
|
|
15918
|
+
var state = JSON.parse(localStorage.getItem('marvin-collapsed') || '{}');
|
|
15919
|
+
state[id] = section.classList.contains('collapsed');
|
|
15920
|
+
localStorage.setItem('marvin-collapsed', JSON.stringify(state));
|
|
15921
|
+
} catch(e) {}
|
|
15922
|
+
}
|
|
15923
|
+
}
|
|
15924
|
+
// Restore collapsed state on load
|
|
15925
|
+
(function() {
|
|
15926
|
+
try {
|
|
15927
|
+
var state = JSON.parse(localStorage.getItem('marvin-collapsed') || '{}');
|
|
15928
|
+
document.querySelectorAll('.collapsible[data-section-id]').forEach(function(el) {
|
|
15929
|
+
var id = el.getAttribute('data-section-id');
|
|
15930
|
+
if (state[id] === true) el.classList.add('collapsed');
|
|
15931
|
+
else if (state[id] === false) el.classList.remove('collapsed');
|
|
15932
|
+
});
|
|
15933
|
+
} catch(e) {}
|
|
15934
|
+
})();
|
|
15935
|
+
</script>
|
|
15676
15936
|
<script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
|
|
15677
15937
|
<script>mermaid.initialize({
|
|
15678
15938
|
startOnLoad: true,
|
|
@@ -16496,13 +16756,60 @@ tr:hover td {
|
|
|
16496
16756
|
white-space: nowrap;
|
|
16497
16757
|
}
|
|
16498
16758
|
|
|
16759
|
+
.gantt-grid-line {
|
|
16760
|
+
position: absolute;
|
|
16761
|
+
top: 0;
|
|
16762
|
+
bottom: 0;
|
|
16763
|
+
width: 1px;
|
|
16764
|
+
background: var(--border);
|
|
16765
|
+
opacity: 0.35;
|
|
16766
|
+
}
|
|
16767
|
+
|
|
16768
|
+
.gantt-sprint-line {
|
|
16769
|
+
position: absolute;
|
|
16770
|
+
top: 0;
|
|
16771
|
+
bottom: 0;
|
|
16772
|
+
width: 1px;
|
|
16773
|
+
background: var(--text-dim);
|
|
16774
|
+
opacity: 0.3;
|
|
16775
|
+
}
|
|
16776
|
+
|
|
16499
16777
|
.gantt-today {
|
|
16500
16778
|
position: absolute;
|
|
16501
16779
|
top: 0;
|
|
16502
16780
|
bottom: 0;
|
|
16503
|
-
width:
|
|
16781
|
+
width: 3px;
|
|
16504
16782
|
background: var(--red);
|
|
16505
|
-
opacity: 0.
|
|
16783
|
+
opacity: 0.8;
|
|
16784
|
+
border-radius: 1px;
|
|
16785
|
+
}
|
|
16786
|
+
|
|
16787
|
+
/* Sprint band in timeline */
|
|
16788
|
+
.gantt-sprint-band-row {
|
|
16789
|
+
border-bottom: 1px solid var(--border);
|
|
16790
|
+
margin-bottom: 0.25rem;
|
|
16791
|
+
}
|
|
16792
|
+
|
|
16793
|
+
.gantt-sprint-band {
|
|
16794
|
+
height: 32px;
|
|
16795
|
+
}
|
|
16796
|
+
|
|
16797
|
+
.gantt-sprint-block {
|
|
16798
|
+
position: absolute;
|
|
16799
|
+
top: 2px;
|
|
16800
|
+
bottom: 2px;
|
|
16801
|
+
background: var(--bg-hover);
|
|
16802
|
+
border: 1px solid var(--border);
|
|
16803
|
+
border-radius: 4px;
|
|
16804
|
+
font-size: 0.65rem;
|
|
16805
|
+
color: var(--text-dim);
|
|
16806
|
+
display: flex;
|
|
16807
|
+
align-items: center;
|
|
16808
|
+
justify-content: center;
|
|
16809
|
+
overflow: hidden;
|
|
16810
|
+
white-space: nowrap;
|
|
16811
|
+
text-overflow: ellipsis;
|
|
16812
|
+
padding: 0 0.4rem;
|
|
16506
16813
|
}
|
|
16507
16814
|
|
|
16508
16815
|
/* Pie chart color overrides */
|
|
@@ -16514,6 +16821,90 @@ tr:hover td {
|
|
|
16514
16821
|
fill: var(--bg) !important;
|
|
16515
16822
|
font-weight: 600;
|
|
16516
16823
|
}
|
|
16824
|
+
|
|
16825
|
+
/* Urgency row indicators */
|
|
16826
|
+
.urgency-row-overdue { border-left: 3px solid var(--red); }
|
|
16827
|
+
.urgency-row-due-3d { border-left: 3px solid var(--amber); }
|
|
16828
|
+
.urgency-row-due-7d { border-left: 3px solid #e2a308; }
|
|
16829
|
+
|
|
16830
|
+
/* Urgency badge pills */
|
|
16831
|
+
.urgency-badge-overdue { background: rgba(248, 113, 113, 0.15); color: var(--red); }
|
|
16832
|
+
.urgency-badge-due-3d { background: rgba(251, 191, 36, 0.15); color: var(--amber); }
|
|
16833
|
+
.urgency-badge-due-7d { background: rgba(226, 163, 8, 0.15); color: #e2a308; }
|
|
16834
|
+
.urgency-badge-upcoming { background: rgba(108, 140, 255, 0.15); color: var(--accent); }
|
|
16835
|
+
.urgency-badge-later { background: rgba(139, 143, 164, 0.1); color: var(--text-dim); }
|
|
16836
|
+
|
|
16837
|
+
/* Trending */
|
|
16838
|
+
.trending-rank {
|
|
16839
|
+
display: inline-flex;
|
|
16840
|
+
align-items: center;
|
|
16841
|
+
justify-content: center;
|
|
16842
|
+
width: 24px;
|
|
16843
|
+
height: 24px;
|
|
16844
|
+
border-radius: 50%;
|
|
16845
|
+
background: var(--bg-hover);
|
|
16846
|
+
font-size: 0.75rem;
|
|
16847
|
+
font-weight: 600;
|
|
16848
|
+
color: var(--text-dim);
|
|
16849
|
+
}
|
|
16850
|
+
|
|
16851
|
+
.trending-score {
|
|
16852
|
+
display: inline-block;
|
|
16853
|
+
padding: 0.15rem 0.6rem;
|
|
16854
|
+
border-radius: 999px;
|
|
16855
|
+
font-size: 0.7rem;
|
|
16856
|
+
font-weight: 700;
|
|
16857
|
+
background: rgba(108, 140, 255, 0.15);
|
|
16858
|
+
color: var(--accent);
|
|
16859
|
+
}
|
|
16860
|
+
|
|
16861
|
+
.signal-tag {
|
|
16862
|
+
display: inline-block;
|
|
16863
|
+
padding: 0.1rem 0.45rem;
|
|
16864
|
+
border-radius: 4px;
|
|
16865
|
+
font-size: 0.65rem;
|
|
16866
|
+
background: var(--bg-hover);
|
|
16867
|
+
color: var(--text-dim);
|
|
16868
|
+
margin-right: 0.25rem;
|
|
16869
|
+
margin-bottom: 0.15rem;
|
|
16870
|
+
white-space: nowrap;
|
|
16871
|
+
}
|
|
16872
|
+
|
|
16873
|
+
.text-dim { color: var(--text-dim); }
|
|
16874
|
+
|
|
16875
|
+
/* Collapsible sections */
|
|
16876
|
+
.collapsible-header {
|
|
16877
|
+
cursor: pointer;
|
|
16878
|
+
display: flex;
|
|
16879
|
+
align-items: center;
|
|
16880
|
+
gap: 0.4rem;
|
|
16881
|
+
user-select: none;
|
|
16882
|
+
}
|
|
16883
|
+
|
|
16884
|
+
.collapsible-header:hover {
|
|
16885
|
+
color: var(--accent);
|
|
16886
|
+
}
|
|
16887
|
+
|
|
16888
|
+
.collapsible-chevron {
|
|
16889
|
+
transition: transform 0.2s ease;
|
|
16890
|
+
flex-shrink: 0;
|
|
16891
|
+
}
|
|
16892
|
+
|
|
16893
|
+
.collapsible.collapsed .collapsible-chevron {
|
|
16894
|
+
transform: rotate(-90deg);
|
|
16895
|
+
}
|
|
16896
|
+
|
|
16897
|
+
.collapsible-body {
|
|
16898
|
+
overflow: hidden;
|
|
16899
|
+
max-height: 5000px;
|
|
16900
|
+
transition: max-height 0.3s ease, opacity 0.2s ease;
|
|
16901
|
+
opacity: 1;
|
|
16902
|
+
}
|
|
16903
|
+
|
|
16904
|
+
.collapsible.collapsed .collapsible-body {
|
|
16905
|
+
max-height: 0;
|
|
16906
|
+
opacity: 0;
|
|
16907
|
+
}
|
|
16517
16908
|
`;
|
|
16518
16909
|
}
|
|
16519
16910
|
|
|
@@ -16566,35 +16957,73 @@ function buildTimelineGantt(data, maxSprints = 6) {
|
|
|
16566
16957
|
);
|
|
16567
16958
|
tick += 7 * DAY;
|
|
16568
16959
|
}
|
|
16960
|
+
const gridLines = [];
|
|
16961
|
+
let gridTick = timelineStart;
|
|
16962
|
+
const gridStartDay = new Date(gridTick).getDay();
|
|
16963
|
+
gridTick += (8 - gridStartDay) % 7 * DAY;
|
|
16964
|
+
while (gridTick <= timelineEnd) {
|
|
16965
|
+
gridLines.push(`<div class="gantt-grid-line" style="left:${pct(gridTick).toFixed(2)}%"></div>`);
|
|
16966
|
+
gridTick += 7 * DAY;
|
|
16967
|
+
}
|
|
16968
|
+
const sprintBoundaries = /* @__PURE__ */ new Set();
|
|
16969
|
+
for (const sprint of visibleSprints) {
|
|
16970
|
+
sprintBoundaries.add(toMs(sprint.startDate));
|
|
16971
|
+
sprintBoundaries.add(toMs(sprint.endDate));
|
|
16972
|
+
}
|
|
16973
|
+
const sprintLines = [...sprintBoundaries].map(
|
|
16974
|
+
(ms) => `<div class="gantt-sprint-line" style="left:${pct(ms).toFixed(2)}%"></div>`
|
|
16975
|
+
);
|
|
16569
16976
|
const now = Date.now();
|
|
16570
16977
|
let todayMarker = "";
|
|
16571
16978
|
if (now >= timelineStart && now <= timelineEnd) {
|
|
16572
16979
|
todayMarker = `<div class="gantt-today" style="left:${pct(now).toFixed(2)}%"></div>`;
|
|
16573
16980
|
}
|
|
16574
|
-
const
|
|
16981
|
+
const sprintBlocks = visibleSprints.map((sprint) => {
|
|
16982
|
+
const sStart = toMs(sprint.startDate);
|
|
16983
|
+
const sEnd = toMs(sprint.endDate);
|
|
16984
|
+
const left = pct(sStart).toFixed(2);
|
|
16985
|
+
const width = (pct(sEnd) - pct(sStart)).toFixed(2);
|
|
16986
|
+
return `<div class="gantt-sprint-block" style="left:${left}%;width:${width}%">${sanitize(sprint.id, 20)}</div>`;
|
|
16987
|
+
}).join("");
|
|
16988
|
+
const sprintBandRow = `<div class="gantt-row gantt-sprint-band-row">
|
|
16989
|
+
<div class="gantt-label gantt-section-label">Sprints</div>
|
|
16990
|
+
<div class="gantt-track gantt-sprint-band">${sprintBlocks}</div>
|
|
16991
|
+
</div>`;
|
|
16992
|
+
const epicSpanMap = /* @__PURE__ */ new Map();
|
|
16575
16993
|
for (const sprint of visibleSprints) {
|
|
16576
16994
|
const sStart = toMs(sprint.startDate);
|
|
16577
16995
|
const sEnd = toMs(sprint.endDate);
|
|
16578
|
-
|
|
16579
|
-
|
|
16580
|
-
|
|
16581
|
-
|
|
16582
|
-
|
|
16583
|
-
|
|
16584
|
-
|
|
16585
|
-
|
|
16586
|
-
|
|
16587
|
-
|
|
16588
|
-
|
|
16589
|
-
|
|
16590
|
-
|
|
16591
|
-
|
|
16996
|
+
for (const eid of sprint.linkedEpics) {
|
|
16997
|
+
if (!epicMap.has(eid)) continue;
|
|
16998
|
+
const existing = epicSpanMap.get(eid);
|
|
16999
|
+
if (existing) {
|
|
17000
|
+
existing.startMs = Math.min(existing.startMs, sStart);
|
|
17001
|
+
existing.endMs = Math.max(existing.endMs, sEnd);
|
|
17002
|
+
} else {
|
|
17003
|
+
epicSpanMap.set(eid, { startMs: sStart, endMs: sEnd });
|
|
17004
|
+
}
|
|
17005
|
+
}
|
|
17006
|
+
}
|
|
17007
|
+
const sortedEpicIds = [...epicSpanMap.keys()].sort((a, b) => {
|
|
17008
|
+
const aSpan = epicSpanMap.get(a);
|
|
17009
|
+
const bSpan = epicSpanMap.get(b);
|
|
17010
|
+
if (aSpan.startMs !== bSpan.startMs) return aSpan.startMs - bSpan.startMs;
|
|
17011
|
+
return a.localeCompare(b);
|
|
17012
|
+
});
|
|
17013
|
+
const epicRows = sortedEpicIds.map((eid) => {
|
|
17014
|
+
const epic = epicMap.get(eid);
|
|
17015
|
+
const { startMs, endMs } = epicSpanMap.get(eid);
|
|
17016
|
+
const cls = epic.status === "done" || epic.status === "completed" ? "gantt-bar-done" : epic.status === "in-progress" || epic.status === "active" ? "gantt-bar-active" : epic.status === "blocked" ? "gantt-bar-blocked" : "gantt-bar-default";
|
|
17017
|
+
const left = pct(startMs).toFixed(2);
|
|
17018
|
+
const width = (pct(endMs) - pct(startMs)).toFixed(2);
|
|
17019
|
+
const label = sanitize(epic.id + " " + epic.title);
|
|
17020
|
+
return `<div class="gantt-row">
|
|
17021
|
+
<div class="gantt-label">${label}</div>
|
|
16592
17022
|
<div class="gantt-track">
|
|
16593
17023
|
<div class="gantt-bar ${cls}" style="left:${left}%;width:${width}%"></div>
|
|
16594
17024
|
</div>
|
|
16595
|
-
</div
|
|
16596
|
-
|
|
16597
|
-
}
|
|
17025
|
+
</div>`;
|
|
17026
|
+
}).join("\n");
|
|
16598
17027
|
const note = truncated ? `<div class="mermaid-note">${hiddenCount} earlier sprint${hiddenCount > 1 ? "s" : ""} not shown</div>` : "";
|
|
16599
17028
|
return `${note}
|
|
16600
17029
|
<div class="gantt">
|
|
@@ -16603,11 +17032,12 @@ function buildTimelineGantt(data, maxSprints = 6) {
|
|
|
16603
17032
|
<div class="gantt-label"></div>
|
|
16604
17033
|
<div class="gantt-track gantt-dates">${markers.join("")}</div>
|
|
16605
17034
|
</div>
|
|
16606
|
-
${
|
|
17035
|
+
${sprintBandRow}
|
|
17036
|
+
${epicRows}
|
|
16607
17037
|
</div>
|
|
16608
17038
|
<div class="gantt-overlay">
|
|
16609
17039
|
<div class="gantt-label"></div>
|
|
16610
|
-
<div class="gantt-track">${todayMarker}</div>
|
|
17040
|
+
<div class="gantt-track">${gridLines.join("")}${sprintLines.join("")}${todayMarker}</div>
|
|
16611
17041
|
</div>
|
|
16612
17042
|
</div>`;
|
|
16613
17043
|
}
|
|
@@ -16686,13 +17116,14 @@ function buildArtifactFlowchart(data) {
|
|
|
16686
17116
|
var svg = document.getElementById('flow-lines');
|
|
16687
17117
|
if (!container || !svg) return;
|
|
16688
17118
|
|
|
16689
|
-
// Build adjacency
|
|
16690
|
-
var
|
|
17119
|
+
// Build directed adjacency maps for traversal
|
|
17120
|
+
var fwd = {}; // from \u2192 [to] (Feature\u2192Epic, Epic\u2192Sprint)
|
|
17121
|
+
var bwd = {}; // to \u2192 [from] (Sprint\u2192Epic, Epic\u2192Feature)
|
|
16691
17122
|
edges.forEach(function(e) {
|
|
16692
|
-
if (!
|
|
16693
|
-
if (!
|
|
16694
|
-
|
|
16695
|
-
|
|
17123
|
+
if (!fwd[e.from]) fwd[e.from] = [];
|
|
17124
|
+
if (!bwd[e.to]) bwd[e.to] = [];
|
|
17125
|
+
fwd[e.from].push(e.to);
|
|
17126
|
+
bwd[e.to].push(e.from);
|
|
16696
17127
|
});
|
|
16697
17128
|
|
|
16698
17129
|
function drawLines() {
|
|
@@ -16725,14 +17156,28 @@ function buildArtifactFlowchart(data) {
|
|
|
16725
17156
|
});
|
|
16726
17157
|
}
|
|
16727
17158
|
|
|
16728
|
-
// Find
|
|
17159
|
+
// Find directly related nodes via directed traversal
|
|
17160
|
+
// Follows forward edges (Feature\u2192Epic\u2192Sprint) and backward edges
|
|
17161
|
+
// (Sprint\u2192Epic\u2192Feature) separately to avoid sideways expansion
|
|
16729
17162
|
function findConnected(startId) {
|
|
16730
17163
|
var visited = {};
|
|
16731
|
-
var queue = [startId];
|
|
16732
17164
|
visited[startId] = true;
|
|
17165
|
+
// Traverse forward (from\u2192to direction)
|
|
17166
|
+
var queue = [startId];
|
|
17167
|
+
while (queue.length) {
|
|
17168
|
+
var id = queue.shift();
|
|
17169
|
+
(fwd[id] || []).forEach(function(neighbor) {
|
|
17170
|
+
if (!visited[neighbor]) {
|
|
17171
|
+
visited[neighbor] = true;
|
|
17172
|
+
queue.push(neighbor);
|
|
17173
|
+
}
|
|
17174
|
+
});
|
|
17175
|
+
}
|
|
17176
|
+
// Traverse backward (to\u2192from direction)
|
|
17177
|
+
queue = [startId];
|
|
16733
17178
|
while (queue.length) {
|
|
16734
17179
|
var id = queue.shift();
|
|
16735
|
-
(
|
|
17180
|
+
(bwd[id] || []).forEach(function(neighbor) {
|
|
16736
17181
|
if (!visited[neighbor]) {
|
|
16737
17182
|
visited[neighbor] = true;
|
|
16738
17183
|
queue.push(neighbor);
|
|
@@ -16872,11 +17317,12 @@ function overviewPage(data, diagrams, navGroups) {
|
|
|
16872
17317
|
|
|
16873
17318
|
<div class="section-title"><a href="/timeline">Project Timeline →</a></div>
|
|
16874
17319
|
|
|
16875
|
-
|
|
16876
|
-
${buildArtifactFlowchart(diagrams)}
|
|
17320
|
+
${collapsibleSection("overview-relationships", "Artifact Relationships", buildArtifactFlowchart(diagrams))}
|
|
16877
17321
|
|
|
16878
|
-
|
|
16879
|
-
|
|
17322
|
+
${collapsibleSection(
|
|
17323
|
+
"overview-recent",
|
|
17324
|
+
"Recent Activity",
|
|
17325
|
+
data.recent.length > 0 ? `
|
|
16880
17326
|
<div class="table-wrap">
|
|
16881
17327
|
<table>
|
|
16882
17328
|
<thead>
|
|
@@ -16892,7 +17338,8 @@ function overviewPage(data, diagrams, navGroups) {
|
|
|
16892
17338
|
${rows}
|
|
16893
17339
|
</tbody>
|
|
16894
17340
|
</table>
|
|
16895
|
-
</div>` : `<div class="empty"><p>No documents yet.</p></div>`
|
|
17341
|
+
</div>` : `<div class="empty"><p>No documents yet.</p></div>`
|
|
17342
|
+
)}
|
|
16896
17343
|
`;
|
|
16897
17344
|
}
|
|
16898
17345
|
|
|
@@ -17037,23 +17484,24 @@ function garPage(report) {
|
|
|
17037
17484
|
<div class="label">Overall: ${escapeHtml(report.overall)}</div>
|
|
17038
17485
|
</div>
|
|
17039
17486
|
|
|
17040
|
-
|
|
17041
|
-
${areaCards}
|
|
17042
|
-
</div>
|
|
17487
|
+
${collapsibleSection("gar-areas", "Areas", `<div class="gar-areas">${areaCards}</div>`)}
|
|
17043
17488
|
|
|
17044
|
-
|
|
17045
|
-
|
|
17046
|
-
|
|
17047
|
-
|
|
17048
|
-
|
|
17049
|
-
|
|
17489
|
+
${collapsibleSection(
|
|
17490
|
+
"gar-status-dist",
|
|
17491
|
+
"Status Distribution",
|
|
17492
|
+
buildStatusPie("Action Status", {
|
|
17493
|
+
Open: report.metrics.scope.open,
|
|
17494
|
+
Done: report.metrics.scope.done,
|
|
17495
|
+
"In Progress": Math.max(0, report.metrics.scope.total - report.metrics.scope.open - report.metrics.scope.done)
|
|
17496
|
+
})
|
|
17497
|
+
)}
|
|
17050
17498
|
`;
|
|
17051
17499
|
}
|
|
17052
17500
|
|
|
17053
17501
|
// src/web/templates/pages/health.ts
|
|
17054
17502
|
function healthPage(report, metrics) {
|
|
17055
17503
|
const dotClass = `dot-${report.overall}`;
|
|
17056
|
-
function renderSection(title, categories) {
|
|
17504
|
+
function renderSection(sectionId, title, categories) {
|
|
17057
17505
|
const cards = categories.map(
|
|
17058
17506
|
(cat) => `
|
|
17059
17507
|
<div class="gar-area">
|
|
@@ -17065,10 +17513,9 @@ function healthPage(report, metrics) {
|
|
|
17065
17513
|
${cat.items.length > 0 ? `<ul>${cat.items.map((item) => `<li><span class="ref-id">${escapeHtml(item.id)}</span>${escapeHtml(item.detail)}</li>`).join("")}</ul>` : ""}
|
|
17066
17514
|
</div>`
|
|
17067
17515
|
).join("\n");
|
|
17068
|
-
return
|
|
17069
|
-
|
|
17070
|
-
|
|
17071
|
-
`;
|
|
17516
|
+
return collapsibleSection(sectionId, title, `<div class="gar-areas">${cards}</div>`, {
|
|
17517
|
+
titleClass: "health-section-title"
|
|
17518
|
+
});
|
|
17072
17519
|
}
|
|
17073
17520
|
return `
|
|
17074
17521
|
<div class="page-header">
|
|
@@ -17081,35 +17528,43 @@ function healthPage(report, metrics) {
|
|
|
17081
17528
|
<div class="label">Overall: ${escapeHtml(report.overall)}</div>
|
|
17082
17529
|
</div>
|
|
17083
17530
|
|
|
17084
|
-
${renderSection("Completeness", report.completeness)}
|
|
17085
|
-
|
|
17086
|
-
|
|
17087
|
-
|
|
17088
|
-
|
|
17089
|
-
|
|
17090
|
-
|
|
17091
|
-
|
|
17092
|
-
|
|
17093
|
-
|
|
17094
|
-
|
|
17095
|
-
|
|
17096
|
-
|
|
17097
|
-
|
|
17098
|
-
|
|
17099
|
-
|
|
17531
|
+
${renderSection("health-completeness", "Completeness", report.completeness)}
|
|
17532
|
+
|
|
17533
|
+
${collapsibleSection(
|
|
17534
|
+
"health-completeness-overview",
|
|
17535
|
+
"Completeness Overview",
|
|
17536
|
+
buildHealthGauge(
|
|
17537
|
+
metrics ? Object.entries(metrics.completeness).map(([name, cat]) => ({
|
|
17538
|
+
name: name.replace(/\b\w/g, (c) => c.toUpperCase()),
|
|
17539
|
+
complete: cat.complete,
|
|
17540
|
+
total: cat.total
|
|
17541
|
+
})) : report.completeness.map((c) => {
|
|
17542
|
+
const match = c.summary.match(/(\d+)\s*\/\s*(\d+)/);
|
|
17543
|
+
return {
|
|
17544
|
+
name: c.name,
|
|
17545
|
+
complete: match ? parseInt(match[1], 10) : 0,
|
|
17546
|
+
total: match ? parseInt(match[2], 10) : 0
|
|
17547
|
+
};
|
|
17548
|
+
})
|
|
17549
|
+
),
|
|
17550
|
+
{ titleClass: "health-section-title" }
|
|
17100
17551
|
)}
|
|
17101
17552
|
|
|
17102
|
-
${renderSection("Process", report.process)}
|
|
17103
|
-
|
|
17104
|
-
|
|
17105
|
-
|
|
17106
|
-
|
|
17107
|
-
"
|
|
17108
|
-
|
|
17109
|
-
|
|
17110
|
-
|
|
17111
|
-
|
|
17112
|
-
|
|
17553
|
+
${renderSection("health-process", "Process", report.process)}
|
|
17554
|
+
|
|
17555
|
+
${collapsibleSection(
|
|
17556
|
+
"health-process-summary",
|
|
17557
|
+
"Process Summary",
|
|
17558
|
+
metrics ? buildStatusPie("Process Health", {
|
|
17559
|
+
Stale: metrics.process.stale.length,
|
|
17560
|
+
"Aging Actions": metrics.process.agingActions.length,
|
|
17561
|
+
Healthy: Math.max(
|
|
17562
|
+
0,
|
|
17563
|
+
(metrics.completeness ? Object.values(metrics.completeness).reduce((sum, c) => sum + c.total, 0) : 0) - metrics.process.stale.length - metrics.process.agingActions.length
|
|
17564
|
+
)
|
|
17565
|
+
}) : "",
|
|
17566
|
+
{ titleClass: "health-section-title" }
|
|
17567
|
+
)}
|
|
17113
17568
|
`;
|
|
17114
17569
|
}
|
|
17115
17570
|
|
|
@@ -17167,13 +17622,147 @@ function timelinePage(diagrams) {
|
|
|
17167
17622
|
return `
|
|
17168
17623
|
<div class="page-header">
|
|
17169
17624
|
<h2>Project Timeline</h2>
|
|
17170
|
-
<div class="subtitle">
|
|
17625
|
+
<div class="subtitle">Epic timeline across sprints</div>
|
|
17171
17626
|
</div>
|
|
17172
17627
|
|
|
17173
17628
|
${buildTimelineGantt(diagrams)}
|
|
17174
17629
|
`;
|
|
17175
17630
|
}
|
|
17176
17631
|
|
|
17632
|
+
// src/web/templates/pages/upcoming.ts
|
|
17633
|
+
function urgencyBadge(tier) {
|
|
17634
|
+
const labels = {
|
|
17635
|
+
overdue: "Overdue",
|
|
17636
|
+
"due-3d": "Due in 3d",
|
|
17637
|
+
"due-7d": "Due in 7d",
|
|
17638
|
+
upcoming: "Upcoming",
|
|
17639
|
+
later: "Later"
|
|
17640
|
+
};
|
|
17641
|
+
return `<span class="badge urgency-badge-${tier}">${labels[tier]}</span>`;
|
|
17642
|
+
}
|
|
17643
|
+
function urgencyRowClass(tier) {
|
|
17644
|
+
if (tier === "overdue") return " urgency-row-overdue";
|
|
17645
|
+
if (tier === "due-3d") return " urgency-row-due-3d";
|
|
17646
|
+
if (tier === "due-7d") return " urgency-row-due-7d";
|
|
17647
|
+
return "";
|
|
17648
|
+
}
|
|
17649
|
+
function upcomingPage(data) {
|
|
17650
|
+
const hasActions = data.dueSoonActions.length > 0;
|
|
17651
|
+
const hasSprintTasks = data.dueSoonSprintTasks.length > 0;
|
|
17652
|
+
const hasTrending = data.trending.length > 0;
|
|
17653
|
+
const actionsTable = hasActions ? collapsibleSection(
|
|
17654
|
+
"upcoming-actions",
|
|
17655
|
+
"Due Soon \u2014 Actions",
|
|
17656
|
+
`<div class="table-wrap">
|
|
17657
|
+
<table>
|
|
17658
|
+
<thead>
|
|
17659
|
+
<tr>
|
|
17660
|
+
<th>ID</th>
|
|
17661
|
+
<th>Title</th>
|
|
17662
|
+
<th>Status</th>
|
|
17663
|
+
<th>Owner</th>
|
|
17664
|
+
<th>Due Date</th>
|
|
17665
|
+
<th>Urgency</th>
|
|
17666
|
+
<th>Tasks</th>
|
|
17667
|
+
</tr>
|
|
17668
|
+
</thead>
|
|
17669
|
+
<tbody>
|
|
17670
|
+
${data.dueSoonActions.map(
|
|
17671
|
+
(a) => `
|
|
17672
|
+
<tr class="${urgencyRowClass(a.urgency)}">
|
|
17673
|
+
<td><a href="/docs/action/${escapeHtml(a.id)}">${escapeHtml(a.id)}</a></td>
|
|
17674
|
+
<td>${escapeHtml(a.title)}</td>
|
|
17675
|
+
<td>${statusBadge(a.status)}</td>
|
|
17676
|
+
<td>${a.owner ? escapeHtml(a.owner) : '<span class="text-dim">\u2014</span>'}</td>
|
|
17677
|
+
<td>${formatDate(a.dueDate)}</td>
|
|
17678
|
+
<td>${urgencyBadge(a.urgency)}</td>
|
|
17679
|
+
<td>${a.relatedTaskCount > 0 ? a.relatedTaskCount : "\u2014"}</td>
|
|
17680
|
+
</tr>`
|
|
17681
|
+
).join("")}
|
|
17682
|
+
</tbody>
|
|
17683
|
+
</table>
|
|
17684
|
+
</div>`,
|
|
17685
|
+
{ titleTag: "h3" }
|
|
17686
|
+
) : "";
|
|
17687
|
+
const sprintTasksTable = hasSprintTasks ? collapsibleSection(
|
|
17688
|
+
"upcoming-sprint-tasks",
|
|
17689
|
+
"Due Soon \u2014 Sprint Tasks",
|
|
17690
|
+
`<div class="table-wrap">
|
|
17691
|
+
<table>
|
|
17692
|
+
<thead>
|
|
17693
|
+
<tr>
|
|
17694
|
+
<th>ID</th>
|
|
17695
|
+
<th>Title</th>
|
|
17696
|
+
<th>Status</th>
|
|
17697
|
+
<th>Sprint</th>
|
|
17698
|
+
<th>Sprint Ends</th>
|
|
17699
|
+
<th>Urgency</th>
|
|
17700
|
+
</tr>
|
|
17701
|
+
</thead>
|
|
17702
|
+
<tbody>
|
|
17703
|
+
${data.dueSoonSprintTasks.map(
|
|
17704
|
+
(t) => `
|
|
17705
|
+
<tr class="${urgencyRowClass(t.urgency)}">
|
|
17706
|
+
<td><a href="/docs/task/${escapeHtml(t.id)}">${escapeHtml(t.id)}</a></td>
|
|
17707
|
+
<td>${escapeHtml(t.title)}</td>
|
|
17708
|
+
<td>${statusBadge(t.status)}</td>
|
|
17709
|
+
<td><a href="/docs/sprint/${escapeHtml(t.sprintId)}">${escapeHtml(t.sprintId)}</a></td>
|
|
17710
|
+
<td>${formatDate(t.sprintEndDate)}</td>
|
|
17711
|
+
<td>${urgencyBadge(t.urgency)}</td>
|
|
17712
|
+
</tr>`
|
|
17713
|
+
).join("")}
|
|
17714
|
+
</tbody>
|
|
17715
|
+
</table>
|
|
17716
|
+
</div>`,
|
|
17717
|
+
{ titleTag: "h3" }
|
|
17718
|
+
) : "";
|
|
17719
|
+
const trendingTable = hasTrending ? collapsibleSection(
|
|
17720
|
+
"upcoming-trending",
|
|
17721
|
+
"Trending",
|
|
17722
|
+
`<div class="table-wrap">
|
|
17723
|
+
<table>
|
|
17724
|
+
<thead>
|
|
17725
|
+
<tr>
|
|
17726
|
+
<th>#</th>
|
|
17727
|
+
<th>ID</th>
|
|
17728
|
+
<th>Title</th>
|
|
17729
|
+
<th>Type</th>
|
|
17730
|
+
<th>Status</th>
|
|
17731
|
+
<th>Score</th>
|
|
17732
|
+
<th>Signals</th>
|
|
17733
|
+
</tr>
|
|
17734
|
+
</thead>
|
|
17735
|
+
<tbody>
|
|
17736
|
+
${data.trending.map(
|
|
17737
|
+
(t, i) => `
|
|
17738
|
+
<tr>
|
|
17739
|
+
<td><span class="trending-rank">${i + 1}</span></td>
|
|
17740
|
+
<td><a href="/docs/${escapeHtml(t.type)}/${escapeHtml(t.id)}">${escapeHtml(t.id)}</a></td>
|
|
17741
|
+
<td>${escapeHtml(t.title)}</td>
|
|
17742
|
+
<td>${escapeHtml(typeLabel(t.type))}</td>
|
|
17743
|
+
<td>${statusBadge(t.status)}</td>
|
|
17744
|
+
<td><span class="trending-score">${t.score}</span></td>
|
|
17745
|
+
<td>${t.signals.map((s) => `<span class="signal-tag">${escapeHtml(s.factor)} +${s.points}</span>`).join(" ")}</td>
|
|
17746
|
+
</tr>`
|
|
17747
|
+
).join("")}
|
|
17748
|
+
</tbody>
|
|
17749
|
+
</table>
|
|
17750
|
+
</div>`,
|
|
17751
|
+
{ titleTag: "h3" }
|
|
17752
|
+
) : "";
|
|
17753
|
+
const emptyState = !hasActions && !hasSprintTasks && !hasTrending ? '<div class="empty"><p>No upcoming items or trending activity found.</p></div>' : "";
|
|
17754
|
+
return `
|
|
17755
|
+
<div class="page-header">
|
|
17756
|
+
<h2>Upcoming</h2>
|
|
17757
|
+
<div class="subtitle">Time-sensitive items and trending activity</div>
|
|
17758
|
+
</div>
|
|
17759
|
+
${actionsTable}
|
|
17760
|
+
${sprintTasksTable}
|
|
17761
|
+
${trendingTable}
|
|
17762
|
+
${emptyState}
|
|
17763
|
+
`;
|
|
17764
|
+
}
|
|
17765
|
+
|
|
17177
17766
|
// src/web/router.ts
|
|
17178
17767
|
function handleRequest(req, res, store, projectName, navGroups) {
|
|
17179
17768
|
const parsed = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
@@ -17214,6 +17803,12 @@ function handleRequest(req, res, store, projectName, navGroups) {
|
|
|
17214
17803
|
respond(res, layout({ title: "Health Check", activePath: "/health", projectName, navGroups }, body));
|
|
17215
17804
|
return;
|
|
17216
17805
|
}
|
|
17806
|
+
if (pathname === "/upcoming") {
|
|
17807
|
+
const data = getUpcomingData(store);
|
|
17808
|
+
const body = upcomingPage(data);
|
|
17809
|
+
respond(res, layout({ title: "Upcoming", activePath: "/upcoming", projectName, navGroups }, body));
|
|
17810
|
+
return;
|
|
17811
|
+
}
|
|
17217
17812
|
const boardMatch = pathname.match(/^\/board(?:\/([^/]+))?$/);
|
|
17218
17813
|
if (boardMatch) {
|
|
17219
17814
|
const type = boardMatch[1];
|
|
@@ -18586,26 +19181,6 @@ function createSprintPlanningTools(store) {
|
|
|
18586
19181
|
|
|
18587
19182
|
// src/plugins/builtin/tools/tasks.ts
|
|
18588
19183
|
import { tool as tool14 } from "@anthropic-ai/claude-agent-sdk";
|
|
18589
|
-
|
|
18590
|
-
// src/plugins/builtin/tools/task-utils.ts
|
|
18591
|
-
function normalizeLinkedEpics(value) {
|
|
18592
|
-
if (value === void 0 || value === null) return [];
|
|
18593
|
-
if (typeof value === "string") {
|
|
18594
|
-
try {
|
|
18595
|
-
const parsed = JSON.parse(value);
|
|
18596
|
-
if (Array.isArray(parsed)) return parsed.filter((v) => typeof v === "string");
|
|
18597
|
-
} catch {
|
|
18598
|
-
}
|
|
18599
|
-
return [value];
|
|
18600
|
-
}
|
|
18601
|
-
if (Array.isArray(value)) return value.filter((v) => typeof v === "string");
|
|
18602
|
-
return [];
|
|
18603
|
-
}
|
|
18604
|
-
function generateEpicTags(epics) {
|
|
18605
|
-
return epics.map((id) => `epic:${id}`);
|
|
18606
|
-
}
|
|
18607
|
-
|
|
18608
|
-
// src/plugins/builtin/tools/tasks.ts
|
|
18609
19184
|
var linkedEpicArray = external_exports.preprocess(
|
|
18610
19185
|
(val) => {
|
|
18611
19186
|
if (typeof val === "string") {
|
|
@@ -20024,8 +20599,9 @@ function findByJiraKey(store, jiraKey) {
|
|
|
20024
20599
|
const docs = store.list({ type: JIRA_TYPE });
|
|
20025
20600
|
return docs.find((d) => d.frontmatter.jiraKey === jiraKey);
|
|
20026
20601
|
}
|
|
20027
|
-
function createJiraTools(store) {
|
|
20602
|
+
function createJiraTools(store, projectConfig) {
|
|
20028
20603
|
const jiraUserConfig = loadUserConfig().jira;
|
|
20604
|
+
const defaultProjectKey = projectConfig?.jira?.projectKey;
|
|
20029
20605
|
return [
|
|
20030
20606
|
// --- Local read tools ---
|
|
20031
20607
|
tool20(
|
|
@@ -20189,10 +20765,22 @@ function createJiraTools(store) {
|
|
|
20189
20765
|
"Create a Jira issue from any Marvin artifact (D/A/Q/F/E) and create a tracking JI-xxx document",
|
|
20190
20766
|
{
|
|
20191
20767
|
artifactId: external_exports.string().describe("Marvin artifact ID (e.g. 'D-001', 'F-003', 'E-002')"),
|
|
20192
|
-
projectKey: external_exports.string().describe("Jira project key (e.g. 'PROJ')"),
|
|
20768
|
+
projectKey: external_exports.string().optional().describe("Jira project key (e.g. 'PROJ'). Falls back to jira.projectKey from .marvin/config.yaml if not provided."),
|
|
20193
20769
|
issueType: external_exports.enum(["Story", "Task", "Bug", "Epic"]).optional().describe("Jira issue type (default: 'Task')")
|
|
20194
20770
|
},
|
|
20195
20771
|
async (args) => {
|
|
20772
|
+
const resolvedProjectKey = args.projectKey ?? defaultProjectKey;
|
|
20773
|
+
if (!resolvedProjectKey) {
|
|
20774
|
+
return {
|
|
20775
|
+
content: [
|
|
20776
|
+
{
|
|
20777
|
+
type: "text",
|
|
20778
|
+
text: "No projectKey provided and no default configured. Either pass projectKey or set jira.projectKey in .marvin/config.yaml."
|
|
20779
|
+
}
|
|
20780
|
+
],
|
|
20781
|
+
isError: true
|
|
20782
|
+
};
|
|
20783
|
+
}
|
|
20196
20784
|
const jira = createJiraClient(jiraUserConfig);
|
|
20197
20785
|
if (!jira) return jiraNotConfiguredError();
|
|
20198
20786
|
const artifact = store.get(args.artifactId);
|
|
@@ -20212,7 +20800,7 @@ function createJiraTools(store) {
|
|
|
20212
20800
|
`Status: ${artifact.frontmatter.status}`
|
|
20213
20801
|
].join("\n");
|
|
20214
20802
|
const jiraResult = await jira.client.createIssue({
|
|
20215
|
-
project: { key:
|
|
20803
|
+
project: { key: resolvedProjectKey },
|
|
20216
20804
|
summary: artifact.frontmatter.title,
|
|
20217
20805
|
description,
|
|
20218
20806
|
issuetype: { name: args.issueType ?? "Task" }
|
|
@@ -20353,14 +20941,14 @@ var jiraSkill = {
|
|
|
20353
20941
|
documentTypeRegistrations: [
|
|
20354
20942
|
{ type: "jira-issue", dirName: "jira-issues", idPrefix: "JI" }
|
|
20355
20943
|
],
|
|
20356
|
-
tools: (store) => createJiraTools(store),
|
|
20944
|
+
tools: (store, projectConfig) => createJiraTools(store, projectConfig),
|
|
20357
20945
|
promptFragments: {
|
|
20358
20946
|
"product-owner": `You have the **Jira Integration** skill. You can pull issues from Jira and push Marvin artifacts to Jira.
|
|
20359
20947
|
|
|
20360
20948
|
**Available tools:**
|
|
20361
20949
|
- \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues
|
|
20362
20950
|
- \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import issues from Jira by key or JQL query
|
|
20363
|
-
- \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, feature, etc.)
|
|
20951
|
+
- \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, feature, etc.). The \`projectKey\` parameter is optional when a default is configured in \`.marvin/config.yaml\` under \`jira.projectKey\`.
|
|
20364
20952
|
- \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
|
|
20365
20953
|
- \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx
|
|
20366
20954
|
|
|
@@ -20374,7 +20962,7 @@ var jiraSkill = {
|
|
|
20374
20962
|
**Available tools:**
|
|
20375
20963
|
- \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues
|
|
20376
20964
|
- \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import issues from Jira by key or JQL query
|
|
20377
|
-
- \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, action, epic, task, etc.)
|
|
20965
|
+
- \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, action, epic, task, etc.). The \`projectKey\` parameter is optional when a default is configured in \`.marvin/config.yaml\` under \`jira.projectKey\`.
|
|
20378
20966
|
- \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
|
|
20379
20967
|
- \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx
|
|
20380
20968
|
|
|
@@ -20388,7 +20976,7 @@ var jiraSkill = {
|
|
|
20388
20976
|
**Available tools:**
|
|
20389
20977
|
- \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues
|
|
20390
20978
|
- \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import issues from Jira by key or JQL query
|
|
20391
|
-
- \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, action, etc.)
|
|
20979
|
+
- \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, action, etc.). The \`projectKey\` parameter is optional when a default is configured in \`.marvin/config.yaml\` under \`jira.projectKey\`.
|
|
20392
20980
|
- \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
|
|
20393
20981
|
- \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx
|
|
20394
20982
|
|
|
@@ -20459,8 +21047,8 @@ function gatherContext(store, focusFeature, includeDecisions = true, includeQues
|
|
|
20459
21047
|
title: e.frontmatter.title,
|
|
20460
21048
|
status: e.frontmatter.status,
|
|
20461
21049
|
linkedFeature: normalizeLinkedFeatures(e.frontmatter.linkedFeature),
|
|
20462
|
-
targetDate: e.frontmatter.targetDate
|
|
20463
|
-
estimatedEffort: e.frontmatter.estimatedEffort
|
|
21050
|
+
targetDate: typeof e.frontmatter.targetDate === "string" ? e.frontmatter.targetDate : null,
|
|
21051
|
+
estimatedEffort: typeof e.frontmatter.estimatedEffort === "string" ? e.frontmatter.estimatedEffort : null,
|
|
20464
21052
|
content: e.content,
|
|
20465
21053
|
linkedTaskCount: tasks.filter(
|
|
20466
21054
|
(t) => normalizeLinkedEpics(t.frontmatter.linkedEpic).includes(e.frontmatter.id)
|
|
@@ -20471,10 +21059,10 @@ function gatherContext(store, focusFeature, includeDecisions = true, includeQues
|
|
|
20471
21059
|
title: t.frontmatter.title,
|
|
20472
21060
|
status: t.frontmatter.status,
|
|
20473
21061
|
linkedEpic: normalizeLinkedEpics(t.frontmatter.linkedEpic),
|
|
20474
|
-
acceptanceCriteria: t.frontmatter.acceptanceCriteria
|
|
20475
|
-
technicalNotes: t.frontmatter.technicalNotes
|
|
20476
|
-
complexity: t.frontmatter.complexity
|
|
20477
|
-
estimatedPoints: t.frontmatter.estimatedPoints
|
|
21062
|
+
acceptanceCriteria: typeof t.frontmatter.acceptanceCriteria === "string" ? t.frontmatter.acceptanceCriteria : null,
|
|
21063
|
+
technicalNotes: typeof t.frontmatter.technicalNotes === "string" ? t.frontmatter.technicalNotes : null,
|
|
21064
|
+
complexity: typeof t.frontmatter.complexity === "string" ? t.frontmatter.complexity : null,
|
|
21065
|
+
estimatedPoints: typeof t.frontmatter.estimatedPoints === "number" ? t.frontmatter.estimatedPoints : null,
|
|
20478
21066
|
priority: t.frontmatter.priority ?? null
|
|
20479
21067
|
})),
|
|
20480
21068
|
decisions: allDecisions.map((d) => ({
|
|
@@ -20966,12 +21554,12 @@ function collectSkillRegistrations(skillIds, allSkills) {
|
|
|
20966
21554
|
}
|
|
20967
21555
|
return registrations;
|
|
20968
21556
|
}
|
|
20969
|
-
function getSkillTools(skillIds, allSkills, store) {
|
|
21557
|
+
function getSkillTools(skillIds, allSkills, store, projectConfig) {
|
|
20970
21558
|
const tools = [];
|
|
20971
21559
|
for (const id of skillIds) {
|
|
20972
21560
|
const skill = allSkills.get(id);
|
|
20973
21561
|
if (skill?.tools) {
|
|
20974
|
-
tools.push(...skill.tools(store));
|
|
21562
|
+
tools.push(...skill.tools(store, projectConfig));
|
|
20975
21563
|
}
|
|
20976
21564
|
}
|
|
20977
21565
|
return tools;
|
|
@@ -21211,6 +21799,7 @@ function createWebTools(store, projectName, navGroups) {
|
|
|
21211
21799
|
const base = `http://localhost:${runningServer.port}`;
|
|
21212
21800
|
const urls = {
|
|
21213
21801
|
overview: base,
|
|
21802
|
+
upcoming: `${base}/upcoming`,
|
|
21214
21803
|
gar: `${base}/gar`,
|
|
21215
21804
|
board: `${base}/board`
|
|
21216
21805
|
};
|
|
@@ -21284,6 +21873,18 @@ function createWebTools(store, projectName, navGroups) {
|
|
|
21284
21873
|
};
|
|
21285
21874
|
},
|
|
21286
21875
|
{ annotations: { readOnlyHint: true } }
|
|
21876
|
+
),
|
|
21877
|
+
tool22(
|
|
21878
|
+
"get_dashboard_upcoming",
|
|
21879
|
+
"Get upcoming data: due-soon actions and sprint tasks, plus trending items scored by relevance signals. Works without the web server running.",
|
|
21880
|
+
{},
|
|
21881
|
+
async () => {
|
|
21882
|
+
const data = getUpcomingData(store);
|
|
21883
|
+
return {
|
|
21884
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
21885
|
+
};
|
|
21886
|
+
},
|
|
21887
|
+
{ annotations: { readOnlyHint: true } }
|
|
21287
21888
|
)
|
|
21288
21889
|
];
|
|
21289
21890
|
}
|
|
@@ -21544,7 +22145,7 @@ async function startSession(options) {
|
|
|
21544
22145
|
const manifest = hasSourcesDir ? new SourceManifestManager(marvinDir) : void 0;
|
|
21545
22146
|
const pluginTools = plugin ? getPluginTools(plugin, store, marvinDir) : [];
|
|
21546
22147
|
const pluginPromptFragment = plugin ? getPluginPromptFragment(plugin, persona.id) : void 0;
|
|
21547
|
-
const codeSkillTools = getSkillTools(skillIds, allSkills, store);
|
|
22148
|
+
const codeSkillTools = getSkillTools(skillIds, allSkills, store, config2.project);
|
|
21548
22149
|
const skillAgents = getSkillAgentDefinitions(skillIds, allSkills);
|
|
21549
22150
|
const skillPromptFragment = getSkillPromptFragment(skillIds, allSkills, persona.id);
|
|
21550
22151
|
const allSkillIds = [...allSkills.keys()];
|
|
@@ -21656,6 +22257,7 @@ Marvin \u2014 ${persona.name}
|
|
|
21656
22257
|
"mcp__marvin-governance__get_dashboard_overview",
|
|
21657
22258
|
"mcp__marvin-governance__get_dashboard_gar",
|
|
21658
22259
|
"mcp__marvin-governance__get_dashboard_board",
|
|
22260
|
+
"mcp__marvin-governance__get_dashboard_upcoming",
|
|
21659
22261
|
...pluginTools.map((t) => `mcp__marvin-governance__${t.name}`),
|
|
21660
22262
|
...codeSkillTools.map((t) => `mcp__marvin-governance__${t.name}`)
|
|
21661
22263
|
]
|
|
@@ -22055,7 +22657,7 @@ function collectTools(marvinDir) {
|
|
|
22055
22657
|
const sessionStore = new SessionStore(marvinDir);
|
|
22056
22658
|
const allSkills = loadAllSkills(marvinDir);
|
|
22057
22659
|
const allSkillIds = [...allSkills.keys()];
|
|
22058
|
-
const codeSkillTools = getSkillTools(allSkillIds, allSkills, store);
|
|
22660
|
+
const codeSkillTools = getSkillTools(allSkillIds, allSkills, store, config2);
|
|
22059
22661
|
const skillsWithActions = allSkillIds.map((id) => allSkills.get(id)).filter((s) => s.actions && s.actions.length > 0);
|
|
22060
22662
|
const projectRoot = path11.dirname(marvinDir);
|
|
22061
22663
|
const actionTools = createSkillActionTools(skillsWithActions, { store, marvinDir, projectRoot });
|
|
@@ -24821,7 +25423,7 @@ function createProgram() {
|
|
|
24821
25423
|
const program = new Command();
|
|
24822
25424
|
program.name("marvin").description(
|
|
24823
25425
|
"AI-powered product development assistant with Product Owner, Delivery Manager, and Technical Lead personas"
|
|
24824
|
-
).version("0.4.
|
|
25426
|
+
).version("0.4.6");
|
|
24825
25427
|
program.command("init").description("Initialize a new Marvin project in the current directory").action(async () => {
|
|
24826
25428
|
await initCommand();
|
|
24827
25429
|
});
|