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/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
|
|
@@ -15286,6 +15176,134 @@ function collectHealthMetrics(store) {
|
|
|
15286
15176
|
};
|
|
15287
15177
|
}
|
|
15288
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
|
|
15304
|
+
};
|
|
15305
|
+
}
|
|
15306
|
+
|
|
15289
15307
|
// src/reports/health/evaluator.ts
|
|
15290
15308
|
function worstStatus2(statuses) {
|
|
15291
15309
|
if (statuses.includes("red")) return "red";
|
|
@@ -15505,6 +15523,204 @@ 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
|
|
15510
15726
|
function escapeHtml(str) {
|
|
@@ -15628,6 +15844,7 @@ function inline(text) {
|
|
|
15628
15844
|
function layout(opts, body) {
|
|
15629
15845
|
const topItems = [
|
|
15630
15846
|
{ href: "/", label: "Overview" },
|
|
15847
|
+
{ href: "/upcoming", label: "Upcoming" },
|
|
15631
15848
|
{ href: "/timeline", label: "Timeline" },
|
|
15632
15849
|
{ href: "/board", label: "Board" },
|
|
15633
15850
|
{ href: "/gar", label: "GAR Report" },
|
|
@@ -16514,6 +16731,56 @@ tr:hover td {
|
|
|
16514
16731
|
fill: var(--bg) !important;
|
|
16515
16732
|
font-weight: 600;
|
|
16516
16733
|
}
|
|
16734
|
+
|
|
16735
|
+
/* Urgency row indicators */
|
|
16736
|
+
.urgency-row-overdue { border-left: 3px solid var(--red); }
|
|
16737
|
+
.urgency-row-due-3d { border-left: 3px solid var(--amber); }
|
|
16738
|
+
.urgency-row-due-7d { border-left: 3px solid #e2a308; }
|
|
16739
|
+
|
|
16740
|
+
/* Urgency badge pills */
|
|
16741
|
+
.urgency-badge-overdue { background: rgba(248, 113, 113, 0.15); color: var(--red); }
|
|
16742
|
+
.urgency-badge-due-3d { background: rgba(251, 191, 36, 0.15); color: var(--amber); }
|
|
16743
|
+
.urgency-badge-due-7d { background: rgba(226, 163, 8, 0.15); color: #e2a308; }
|
|
16744
|
+
.urgency-badge-upcoming { background: rgba(108, 140, 255, 0.15); color: var(--accent); }
|
|
16745
|
+
.urgency-badge-later { background: rgba(139, 143, 164, 0.1); color: var(--text-dim); }
|
|
16746
|
+
|
|
16747
|
+
/* Trending */
|
|
16748
|
+
.trending-rank {
|
|
16749
|
+
display: inline-flex;
|
|
16750
|
+
align-items: center;
|
|
16751
|
+
justify-content: center;
|
|
16752
|
+
width: 24px;
|
|
16753
|
+
height: 24px;
|
|
16754
|
+
border-radius: 50%;
|
|
16755
|
+
background: var(--bg-hover);
|
|
16756
|
+
font-size: 0.75rem;
|
|
16757
|
+
font-weight: 600;
|
|
16758
|
+
color: var(--text-dim);
|
|
16759
|
+
}
|
|
16760
|
+
|
|
16761
|
+
.trending-score {
|
|
16762
|
+
display: inline-block;
|
|
16763
|
+
padding: 0.15rem 0.6rem;
|
|
16764
|
+
border-radius: 999px;
|
|
16765
|
+
font-size: 0.7rem;
|
|
16766
|
+
font-weight: 700;
|
|
16767
|
+
background: rgba(108, 140, 255, 0.15);
|
|
16768
|
+
color: var(--accent);
|
|
16769
|
+
}
|
|
16770
|
+
|
|
16771
|
+
.signal-tag {
|
|
16772
|
+
display: inline-block;
|
|
16773
|
+
padding: 0.1rem 0.45rem;
|
|
16774
|
+
border-radius: 4px;
|
|
16775
|
+
font-size: 0.65rem;
|
|
16776
|
+
background: var(--bg-hover);
|
|
16777
|
+
color: var(--text-dim);
|
|
16778
|
+
margin-right: 0.25rem;
|
|
16779
|
+
margin-bottom: 0.15rem;
|
|
16780
|
+
white-space: nowrap;
|
|
16781
|
+
}
|
|
16782
|
+
|
|
16783
|
+
.text-dim { color: var(--text-dim); }
|
|
16517
16784
|
`;
|
|
16518
16785
|
}
|
|
16519
16786
|
|
|
@@ -16686,13 +16953,14 @@ function buildArtifactFlowchart(data) {
|
|
|
16686
16953
|
var svg = document.getElementById('flow-lines');
|
|
16687
16954
|
if (!container || !svg) return;
|
|
16688
16955
|
|
|
16689
|
-
// Build adjacency
|
|
16690
|
-
var
|
|
16956
|
+
// Build directed adjacency maps for traversal
|
|
16957
|
+
var fwd = {}; // from \u2192 [to] (Feature\u2192Epic, Epic\u2192Sprint)
|
|
16958
|
+
var bwd = {}; // to \u2192 [from] (Sprint\u2192Epic, Epic\u2192Feature)
|
|
16691
16959
|
edges.forEach(function(e) {
|
|
16692
|
-
if (!
|
|
16693
|
-
if (!
|
|
16694
|
-
|
|
16695
|
-
|
|
16960
|
+
if (!fwd[e.from]) fwd[e.from] = [];
|
|
16961
|
+
if (!bwd[e.to]) bwd[e.to] = [];
|
|
16962
|
+
fwd[e.from].push(e.to);
|
|
16963
|
+
bwd[e.to].push(e.from);
|
|
16696
16964
|
});
|
|
16697
16965
|
|
|
16698
16966
|
function drawLines() {
|
|
@@ -16725,14 +16993,28 @@ function buildArtifactFlowchart(data) {
|
|
|
16725
16993
|
});
|
|
16726
16994
|
}
|
|
16727
16995
|
|
|
16728
|
-
// Find
|
|
16996
|
+
// Find directly related nodes via directed traversal
|
|
16997
|
+
// Follows forward edges (Feature\u2192Epic\u2192Sprint) and backward edges
|
|
16998
|
+
// (Sprint\u2192Epic\u2192Feature) separately to avoid sideways expansion
|
|
16729
16999
|
function findConnected(startId) {
|
|
16730
17000
|
var visited = {};
|
|
16731
|
-
var queue = [startId];
|
|
16732
17001
|
visited[startId] = true;
|
|
17002
|
+
// Traverse forward (from\u2192to direction)
|
|
17003
|
+
var queue = [startId];
|
|
16733
17004
|
while (queue.length) {
|
|
16734
17005
|
var id = queue.shift();
|
|
16735
|
-
(
|
|
17006
|
+
(fwd[id] || []).forEach(function(neighbor) {
|
|
17007
|
+
if (!visited[neighbor]) {
|
|
17008
|
+
visited[neighbor] = true;
|
|
17009
|
+
queue.push(neighbor);
|
|
17010
|
+
}
|
|
17011
|
+
});
|
|
17012
|
+
}
|
|
17013
|
+
// Traverse backward (to\u2192from direction)
|
|
17014
|
+
queue = [startId];
|
|
17015
|
+
while (queue.length) {
|
|
17016
|
+
var id = queue.shift();
|
|
17017
|
+
(bwd[id] || []).forEach(function(neighbor) {
|
|
16736
17018
|
if (!visited[neighbor]) {
|
|
16737
17019
|
visited[neighbor] = true;
|
|
16738
17020
|
queue.push(neighbor);
|
|
@@ -17174,6 +17456,131 @@ function timelinePage(diagrams) {
|
|
|
17174
17456
|
`;
|
|
17175
17457
|
}
|
|
17176
17458
|
|
|
17459
|
+
// src/web/templates/pages/upcoming.ts
|
|
17460
|
+
function urgencyBadge(tier) {
|
|
17461
|
+
const labels = {
|
|
17462
|
+
overdue: "Overdue",
|
|
17463
|
+
"due-3d": "Due in 3d",
|
|
17464
|
+
"due-7d": "Due in 7d",
|
|
17465
|
+
upcoming: "Upcoming",
|
|
17466
|
+
later: "Later"
|
|
17467
|
+
};
|
|
17468
|
+
return `<span class="badge urgency-badge-${tier}">${labels[tier]}</span>`;
|
|
17469
|
+
}
|
|
17470
|
+
function urgencyRowClass(tier) {
|
|
17471
|
+
if (tier === "overdue") return " urgency-row-overdue";
|
|
17472
|
+
if (tier === "due-3d") return " urgency-row-due-3d";
|
|
17473
|
+
if (tier === "due-7d") return " urgency-row-due-7d";
|
|
17474
|
+
return "";
|
|
17475
|
+
}
|
|
17476
|
+
function upcomingPage(data) {
|
|
17477
|
+
const hasActions = data.dueSoonActions.length > 0;
|
|
17478
|
+
const hasSprintTasks = data.dueSoonSprintTasks.length > 0;
|
|
17479
|
+
const hasTrending = data.trending.length > 0;
|
|
17480
|
+
const actionsTable = hasActions ? `
|
|
17481
|
+
<h3 class="section-title">Due Soon \u2014 Actions</h3>
|
|
17482
|
+
<div class="table-wrap">
|
|
17483
|
+
<table>
|
|
17484
|
+
<thead>
|
|
17485
|
+
<tr>
|
|
17486
|
+
<th>ID</th>
|
|
17487
|
+
<th>Title</th>
|
|
17488
|
+
<th>Status</th>
|
|
17489
|
+
<th>Owner</th>
|
|
17490
|
+
<th>Due Date</th>
|
|
17491
|
+
<th>Urgency</th>
|
|
17492
|
+
<th>Tasks</th>
|
|
17493
|
+
</tr>
|
|
17494
|
+
</thead>
|
|
17495
|
+
<tbody>
|
|
17496
|
+
${data.dueSoonActions.map(
|
|
17497
|
+
(a) => `
|
|
17498
|
+
<tr class="${urgencyRowClass(a.urgency)}">
|
|
17499
|
+
<td><a href="/docs/action/${escapeHtml(a.id)}">${escapeHtml(a.id)}</a></td>
|
|
17500
|
+
<td>${escapeHtml(a.title)}</td>
|
|
17501
|
+
<td>${statusBadge(a.status)}</td>
|
|
17502
|
+
<td>${a.owner ? escapeHtml(a.owner) : '<span class="text-dim">\u2014</span>'}</td>
|
|
17503
|
+
<td>${formatDate(a.dueDate)}</td>
|
|
17504
|
+
<td>${urgencyBadge(a.urgency)}</td>
|
|
17505
|
+
<td>${a.relatedTaskCount > 0 ? a.relatedTaskCount : "\u2014"}</td>
|
|
17506
|
+
</tr>`
|
|
17507
|
+
).join("")}
|
|
17508
|
+
</tbody>
|
|
17509
|
+
</table>
|
|
17510
|
+
</div>` : "";
|
|
17511
|
+
const sprintTasksTable = hasSprintTasks ? `
|
|
17512
|
+
<h3 class="section-title">Due Soon \u2014 Sprint Tasks</h3>
|
|
17513
|
+
<div class="table-wrap">
|
|
17514
|
+
<table>
|
|
17515
|
+
<thead>
|
|
17516
|
+
<tr>
|
|
17517
|
+
<th>ID</th>
|
|
17518
|
+
<th>Title</th>
|
|
17519
|
+
<th>Status</th>
|
|
17520
|
+
<th>Sprint</th>
|
|
17521
|
+
<th>Sprint Ends</th>
|
|
17522
|
+
<th>Urgency</th>
|
|
17523
|
+
</tr>
|
|
17524
|
+
</thead>
|
|
17525
|
+
<tbody>
|
|
17526
|
+
${data.dueSoonSprintTasks.map(
|
|
17527
|
+
(t) => `
|
|
17528
|
+
<tr class="${urgencyRowClass(t.urgency)}">
|
|
17529
|
+
<td><a href="/docs/task/${escapeHtml(t.id)}">${escapeHtml(t.id)}</a></td>
|
|
17530
|
+
<td>${escapeHtml(t.title)}</td>
|
|
17531
|
+
<td>${statusBadge(t.status)}</td>
|
|
17532
|
+
<td><a href="/docs/sprint/${escapeHtml(t.sprintId)}">${escapeHtml(t.sprintId)}</a></td>
|
|
17533
|
+
<td>${formatDate(t.sprintEndDate)}</td>
|
|
17534
|
+
<td>${urgencyBadge(t.urgency)}</td>
|
|
17535
|
+
</tr>`
|
|
17536
|
+
).join("")}
|
|
17537
|
+
</tbody>
|
|
17538
|
+
</table>
|
|
17539
|
+
</div>` : "";
|
|
17540
|
+
const trendingTable = hasTrending ? `
|
|
17541
|
+
<h3 class="section-title">Trending</h3>
|
|
17542
|
+
<div class="table-wrap">
|
|
17543
|
+
<table>
|
|
17544
|
+
<thead>
|
|
17545
|
+
<tr>
|
|
17546
|
+
<th>#</th>
|
|
17547
|
+
<th>ID</th>
|
|
17548
|
+
<th>Title</th>
|
|
17549
|
+
<th>Type</th>
|
|
17550
|
+
<th>Status</th>
|
|
17551
|
+
<th>Score</th>
|
|
17552
|
+
<th>Signals</th>
|
|
17553
|
+
</tr>
|
|
17554
|
+
</thead>
|
|
17555
|
+
<tbody>
|
|
17556
|
+
${data.trending.map(
|
|
17557
|
+
(t, i) => `
|
|
17558
|
+
<tr>
|
|
17559
|
+
<td><span class="trending-rank">${i + 1}</span></td>
|
|
17560
|
+
<td><a href="/docs/${escapeHtml(t.type)}/${escapeHtml(t.id)}">${escapeHtml(t.id)}</a></td>
|
|
17561
|
+
<td>${escapeHtml(t.title)}</td>
|
|
17562
|
+
<td>${escapeHtml(typeLabel(t.type))}</td>
|
|
17563
|
+
<td>${statusBadge(t.status)}</td>
|
|
17564
|
+
<td><span class="trending-score">${t.score}</span></td>
|
|
17565
|
+
<td>${t.signals.map((s) => `<span class="signal-tag">${escapeHtml(s.factor)} +${s.points}</span>`).join(" ")}</td>
|
|
17566
|
+
</tr>`
|
|
17567
|
+
).join("")}
|
|
17568
|
+
</tbody>
|
|
17569
|
+
</table>
|
|
17570
|
+
</div>` : "";
|
|
17571
|
+
const emptyState = !hasActions && !hasSprintTasks && !hasTrending ? '<div class="empty"><p>No upcoming items or trending activity found.</p></div>' : "";
|
|
17572
|
+
return `
|
|
17573
|
+
<div class="page-header">
|
|
17574
|
+
<h2>Upcoming</h2>
|
|
17575
|
+
<div class="subtitle">Time-sensitive items and trending activity</div>
|
|
17576
|
+
</div>
|
|
17577
|
+
${actionsTable}
|
|
17578
|
+
${sprintTasksTable}
|
|
17579
|
+
${trendingTable}
|
|
17580
|
+
${emptyState}
|
|
17581
|
+
`;
|
|
17582
|
+
}
|
|
17583
|
+
|
|
17177
17584
|
// src/web/router.ts
|
|
17178
17585
|
function handleRequest(req, res, store, projectName, navGroups) {
|
|
17179
17586
|
const parsed = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
@@ -17214,6 +17621,12 @@ function handleRequest(req, res, store, projectName, navGroups) {
|
|
|
17214
17621
|
respond(res, layout({ title: "Health Check", activePath: "/health", projectName, navGroups }, body));
|
|
17215
17622
|
return;
|
|
17216
17623
|
}
|
|
17624
|
+
if (pathname === "/upcoming") {
|
|
17625
|
+
const data = getUpcomingData(store);
|
|
17626
|
+
const body = upcomingPage(data);
|
|
17627
|
+
respond(res, layout({ title: "Upcoming", activePath: "/upcoming", projectName, navGroups }, body));
|
|
17628
|
+
return;
|
|
17629
|
+
}
|
|
17217
17630
|
const boardMatch = pathname.match(/^\/board(?:\/([^/]+))?$/);
|
|
17218
17631
|
if (boardMatch) {
|
|
17219
17632
|
const type = boardMatch[1];
|
|
@@ -18586,26 +18999,6 @@ function createSprintPlanningTools(store) {
|
|
|
18586
18999
|
|
|
18587
19000
|
// src/plugins/builtin/tools/tasks.ts
|
|
18588
19001
|
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
19002
|
var linkedEpicArray = external_exports.preprocess(
|
|
18610
19003
|
(val) => {
|
|
18611
19004
|
if (typeof val === "string") {
|
|
@@ -20024,8 +20417,9 @@ function findByJiraKey(store, jiraKey) {
|
|
|
20024
20417
|
const docs = store.list({ type: JIRA_TYPE });
|
|
20025
20418
|
return docs.find((d) => d.frontmatter.jiraKey === jiraKey);
|
|
20026
20419
|
}
|
|
20027
|
-
function createJiraTools(store) {
|
|
20420
|
+
function createJiraTools(store, projectConfig) {
|
|
20028
20421
|
const jiraUserConfig = loadUserConfig().jira;
|
|
20422
|
+
const defaultProjectKey = projectConfig?.jira?.projectKey;
|
|
20029
20423
|
return [
|
|
20030
20424
|
// --- Local read tools ---
|
|
20031
20425
|
tool20(
|
|
@@ -20189,10 +20583,22 @@ function createJiraTools(store) {
|
|
|
20189
20583
|
"Create a Jira issue from any Marvin artifact (D/A/Q/F/E) and create a tracking JI-xxx document",
|
|
20190
20584
|
{
|
|
20191
20585
|
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')"),
|
|
20586
|
+
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
20587
|
issueType: external_exports.enum(["Story", "Task", "Bug", "Epic"]).optional().describe("Jira issue type (default: 'Task')")
|
|
20194
20588
|
},
|
|
20195
20589
|
async (args) => {
|
|
20590
|
+
const resolvedProjectKey = args.projectKey ?? defaultProjectKey;
|
|
20591
|
+
if (!resolvedProjectKey) {
|
|
20592
|
+
return {
|
|
20593
|
+
content: [
|
|
20594
|
+
{
|
|
20595
|
+
type: "text",
|
|
20596
|
+
text: "No projectKey provided and no default configured. Either pass projectKey or set jira.projectKey in .marvin/config.yaml."
|
|
20597
|
+
}
|
|
20598
|
+
],
|
|
20599
|
+
isError: true
|
|
20600
|
+
};
|
|
20601
|
+
}
|
|
20196
20602
|
const jira = createJiraClient(jiraUserConfig);
|
|
20197
20603
|
if (!jira) return jiraNotConfiguredError();
|
|
20198
20604
|
const artifact = store.get(args.artifactId);
|
|
@@ -20212,7 +20618,7 @@ function createJiraTools(store) {
|
|
|
20212
20618
|
`Status: ${artifact.frontmatter.status}`
|
|
20213
20619
|
].join("\n");
|
|
20214
20620
|
const jiraResult = await jira.client.createIssue({
|
|
20215
|
-
project: { key:
|
|
20621
|
+
project: { key: resolvedProjectKey },
|
|
20216
20622
|
summary: artifact.frontmatter.title,
|
|
20217
20623
|
description,
|
|
20218
20624
|
issuetype: { name: args.issueType ?? "Task" }
|
|
@@ -20353,14 +20759,14 @@ var jiraSkill = {
|
|
|
20353
20759
|
documentTypeRegistrations: [
|
|
20354
20760
|
{ type: "jira-issue", dirName: "jira-issues", idPrefix: "JI" }
|
|
20355
20761
|
],
|
|
20356
|
-
tools: (store) => createJiraTools(store),
|
|
20762
|
+
tools: (store, projectConfig) => createJiraTools(store, projectConfig),
|
|
20357
20763
|
promptFragments: {
|
|
20358
20764
|
"product-owner": `You have the **Jira Integration** skill. You can pull issues from Jira and push Marvin artifacts to Jira.
|
|
20359
20765
|
|
|
20360
20766
|
**Available tools:**
|
|
20361
20767
|
- \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues
|
|
20362
20768
|
- \`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.)
|
|
20769
|
+
- \`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
20770
|
- \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
|
|
20365
20771
|
- \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx
|
|
20366
20772
|
|
|
@@ -20374,7 +20780,7 @@ var jiraSkill = {
|
|
|
20374
20780
|
**Available tools:**
|
|
20375
20781
|
- \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues
|
|
20376
20782
|
- \`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.)
|
|
20783
|
+
- \`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
20784
|
- \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
|
|
20379
20785
|
- \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx
|
|
20380
20786
|
|
|
@@ -20388,7 +20794,7 @@ var jiraSkill = {
|
|
|
20388
20794
|
**Available tools:**
|
|
20389
20795
|
- \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues
|
|
20390
20796
|
- \`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.)
|
|
20797
|
+
- \`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
20798
|
- \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
|
|
20393
20799
|
- \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx
|
|
20394
20800
|
|
|
@@ -20966,12 +21372,12 @@ function collectSkillRegistrations(skillIds, allSkills) {
|
|
|
20966
21372
|
}
|
|
20967
21373
|
return registrations;
|
|
20968
21374
|
}
|
|
20969
|
-
function getSkillTools(skillIds, allSkills, store) {
|
|
21375
|
+
function getSkillTools(skillIds, allSkills, store, projectConfig) {
|
|
20970
21376
|
const tools = [];
|
|
20971
21377
|
for (const id of skillIds) {
|
|
20972
21378
|
const skill = allSkills.get(id);
|
|
20973
21379
|
if (skill?.tools) {
|
|
20974
|
-
tools.push(...skill.tools(store));
|
|
21380
|
+
tools.push(...skill.tools(store, projectConfig));
|
|
20975
21381
|
}
|
|
20976
21382
|
}
|
|
20977
21383
|
return tools;
|
|
@@ -21211,6 +21617,7 @@ function createWebTools(store, projectName, navGroups) {
|
|
|
21211
21617
|
const base = `http://localhost:${runningServer.port}`;
|
|
21212
21618
|
const urls = {
|
|
21213
21619
|
overview: base,
|
|
21620
|
+
upcoming: `${base}/upcoming`,
|
|
21214
21621
|
gar: `${base}/gar`,
|
|
21215
21622
|
board: `${base}/board`
|
|
21216
21623
|
};
|
|
@@ -21284,6 +21691,18 @@ function createWebTools(store, projectName, navGroups) {
|
|
|
21284
21691
|
};
|
|
21285
21692
|
},
|
|
21286
21693
|
{ annotations: { readOnlyHint: true } }
|
|
21694
|
+
),
|
|
21695
|
+
tool22(
|
|
21696
|
+
"get_dashboard_upcoming",
|
|
21697
|
+
"Get upcoming data: due-soon actions and sprint tasks, plus trending items scored by relevance signals. Works without the web server running.",
|
|
21698
|
+
{},
|
|
21699
|
+
async () => {
|
|
21700
|
+
const data = getUpcomingData(store);
|
|
21701
|
+
return {
|
|
21702
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
21703
|
+
};
|
|
21704
|
+
},
|
|
21705
|
+
{ annotations: { readOnlyHint: true } }
|
|
21287
21706
|
)
|
|
21288
21707
|
];
|
|
21289
21708
|
}
|
|
@@ -21544,7 +21963,7 @@ async function startSession(options) {
|
|
|
21544
21963
|
const manifest = hasSourcesDir ? new SourceManifestManager(marvinDir) : void 0;
|
|
21545
21964
|
const pluginTools = plugin ? getPluginTools(plugin, store, marvinDir) : [];
|
|
21546
21965
|
const pluginPromptFragment = plugin ? getPluginPromptFragment(plugin, persona.id) : void 0;
|
|
21547
|
-
const codeSkillTools = getSkillTools(skillIds, allSkills, store);
|
|
21966
|
+
const codeSkillTools = getSkillTools(skillIds, allSkills, store, config2.project);
|
|
21548
21967
|
const skillAgents = getSkillAgentDefinitions(skillIds, allSkills);
|
|
21549
21968
|
const skillPromptFragment = getSkillPromptFragment(skillIds, allSkills, persona.id);
|
|
21550
21969
|
const allSkillIds = [...allSkills.keys()];
|
|
@@ -21656,6 +22075,7 @@ Marvin \u2014 ${persona.name}
|
|
|
21656
22075
|
"mcp__marvin-governance__get_dashboard_overview",
|
|
21657
22076
|
"mcp__marvin-governance__get_dashboard_gar",
|
|
21658
22077
|
"mcp__marvin-governance__get_dashboard_board",
|
|
22078
|
+
"mcp__marvin-governance__get_dashboard_upcoming",
|
|
21659
22079
|
...pluginTools.map((t) => `mcp__marvin-governance__${t.name}`),
|
|
21660
22080
|
...codeSkillTools.map((t) => `mcp__marvin-governance__${t.name}`)
|
|
21661
22081
|
]
|
|
@@ -22055,7 +22475,7 @@ function collectTools(marvinDir) {
|
|
|
22055
22475
|
const sessionStore = new SessionStore(marvinDir);
|
|
22056
22476
|
const allSkills = loadAllSkills(marvinDir);
|
|
22057
22477
|
const allSkillIds = [...allSkills.keys()];
|
|
22058
|
-
const codeSkillTools = getSkillTools(allSkillIds, allSkills, store);
|
|
22478
|
+
const codeSkillTools = getSkillTools(allSkillIds, allSkills, store, config2);
|
|
22059
22479
|
const skillsWithActions = allSkillIds.map((id) => allSkills.get(id)).filter((s) => s.actions && s.actions.length > 0);
|
|
22060
22480
|
const projectRoot = path11.dirname(marvinDir);
|
|
22061
22481
|
const actionTools = createSkillActionTools(skillsWithActions, { store, marvinDir, projectRoot });
|
|
@@ -24821,7 +25241,7 @@ function createProgram() {
|
|
|
24821
25241
|
const program = new Command();
|
|
24822
25242
|
program.name("marvin").description(
|
|
24823
25243
|
"AI-powered product development assistant with Product Owner, Delivery Manager, and Technical Lead personas"
|
|
24824
|
-
).version("0.4.
|
|
25244
|
+
).version("0.4.5");
|
|
24825
25245
|
program.command("init").description("Initialize a new Marvin project in the current directory").action(async () => {
|
|
24826
25246
|
await initCommand();
|
|
24827
25247
|
});
|