mrvn-cli 0.2.5 → 0.2.8
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/README.md +103 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +969 -92
- package/dist/index.js.map +1 -1
- package/dist/marvin-serve.js +836 -65
- package/dist/marvin-serve.js.map +1 -1
- package/dist/marvin.js +994 -117
- package/dist/marvin.js.map +1 -1
- package/package.json +1 -1
package/dist/marvin-serve.js
CHANGED
|
@@ -14971,6 +14971,128 @@ function createMeetingTools(store) {
|
|
|
14971
14971
|
|
|
14972
14972
|
// src/plugins/builtin/tools/reports.ts
|
|
14973
14973
|
import { tool as tool8 } from "@anthropic-ai/claude-agent-sdk";
|
|
14974
|
+
|
|
14975
|
+
// src/reports/gar/collector.ts
|
|
14976
|
+
function collectGarMetrics(store) {
|
|
14977
|
+
const allActions = store.list({ type: "action" });
|
|
14978
|
+
const openActions = allActions.filter((d) => d.frontmatter.status === "open");
|
|
14979
|
+
const doneActions = allActions.filter((d) => d.frontmatter.status === "done");
|
|
14980
|
+
const allDocs = store.list();
|
|
14981
|
+
const blockedItems = allDocs.filter(
|
|
14982
|
+
(d) => d.frontmatter.tags?.includes("blocked")
|
|
14983
|
+
);
|
|
14984
|
+
const overdueItems = allDocs.filter(
|
|
14985
|
+
(d) => d.frontmatter.tags?.includes("overdue")
|
|
14986
|
+
);
|
|
14987
|
+
const openQuestions = store.list({ type: "question", status: "open" });
|
|
14988
|
+
const riskItems = allDocs.filter(
|
|
14989
|
+
(d) => d.frontmatter.tags?.includes("risk")
|
|
14990
|
+
);
|
|
14991
|
+
const unownedActions = openActions.filter((d) => !d.frontmatter.owner);
|
|
14992
|
+
const total = allActions.length;
|
|
14993
|
+
const done = doneActions.length;
|
|
14994
|
+
const completionPct = total > 0 ? Math.round(done / total * 100) : 100;
|
|
14995
|
+
const scheduleItems = [
|
|
14996
|
+
...blockedItems,
|
|
14997
|
+
...overdueItems
|
|
14998
|
+
].filter(
|
|
14999
|
+
(d, i, arr) => arr.findIndex((x) => x.frontmatter.id === d.frontmatter.id) === i
|
|
15000
|
+
).map((d) => ({ id: d.frontmatter.id, title: d.frontmatter.title }));
|
|
15001
|
+
const qualityItems = [
|
|
15002
|
+
...riskItems,
|
|
15003
|
+
...openQuestions
|
|
15004
|
+
].filter(
|
|
15005
|
+
(d, i, arr) => arr.findIndex((x) => x.frontmatter.id === d.frontmatter.id) === i
|
|
15006
|
+
).map((d) => ({ id: d.frontmatter.id, title: d.frontmatter.title }));
|
|
15007
|
+
const resourceItems = unownedActions.map((d) => ({
|
|
15008
|
+
id: d.frontmatter.id,
|
|
15009
|
+
title: d.frontmatter.title
|
|
15010
|
+
}));
|
|
15011
|
+
return {
|
|
15012
|
+
scope: {
|
|
15013
|
+
total,
|
|
15014
|
+
open: openActions.length,
|
|
15015
|
+
done,
|
|
15016
|
+
completionPct
|
|
15017
|
+
},
|
|
15018
|
+
schedule: {
|
|
15019
|
+
blocked: blockedItems.length,
|
|
15020
|
+
overdue: overdueItems.length,
|
|
15021
|
+
items: scheduleItems
|
|
15022
|
+
},
|
|
15023
|
+
quality: {
|
|
15024
|
+
risks: riskItems.length,
|
|
15025
|
+
openQuestions: openQuestions.length,
|
|
15026
|
+
items: qualityItems
|
|
15027
|
+
},
|
|
15028
|
+
resources: {
|
|
15029
|
+
unowned: unownedActions.length,
|
|
15030
|
+
items: resourceItems
|
|
15031
|
+
}
|
|
15032
|
+
};
|
|
15033
|
+
}
|
|
15034
|
+
|
|
15035
|
+
// src/reports/gar/evaluator.ts
|
|
15036
|
+
function worstStatus(statuses) {
|
|
15037
|
+
if (statuses.includes("red")) return "red";
|
|
15038
|
+
if (statuses.includes("amber")) return "amber";
|
|
15039
|
+
return "green";
|
|
15040
|
+
}
|
|
15041
|
+
function evaluateGar(projectName, metrics) {
|
|
15042
|
+
const areas = [];
|
|
15043
|
+
const scopePct = metrics.scope.completionPct;
|
|
15044
|
+
const scopeStatus = scopePct >= 70 ? "green" : scopePct >= 40 ? "amber" : "red";
|
|
15045
|
+
areas.push({
|
|
15046
|
+
name: "Scope",
|
|
15047
|
+
status: scopeStatus,
|
|
15048
|
+
summary: `${scopePct}% complete (${metrics.scope.done}/${metrics.scope.total})`,
|
|
15049
|
+
items: []
|
|
15050
|
+
});
|
|
15051
|
+
const scheduleCount = metrics.schedule.blocked + metrics.schedule.overdue;
|
|
15052
|
+
const scheduleStatus = scheduleCount === 0 ? "green" : scheduleCount <= 2 ? "amber" : "red";
|
|
15053
|
+
const scheduleParts = [];
|
|
15054
|
+
if (metrics.schedule.blocked > 0)
|
|
15055
|
+
scheduleParts.push(`${metrics.schedule.blocked} blocked`);
|
|
15056
|
+
if (metrics.schedule.overdue > 0)
|
|
15057
|
+
scheduleParts.push(`${metrics.schedule.overdue} overdue`);
|
|
15058
|
+
areas.push({
|
|
15059
|
+
name: "Schedule",
|
|
15060
|
+
status: scheduleStatus,
|
|
15061
|
+
summary: scheduleParts.length > 0 ? scheduleParts.join(", ") : "on track",
|
|
15062
|
+
items: metrics.schedule.items
|
|
15063
|
+
});
|
|
15064
|
+
const qualityCount = metrics.quality.risks + metrics.quality.openQuestions;
|
|
15065
|
+
const qualityStatus = qualityCount === 0 ? "green" : qualityCount <= 2 ? "amber" : "red";
|
|
15066
|
+
const qualityParts = [];
|
|
15067
|
+
if (metrics.quality.risks > 0)
|
|
15068
|
+
qualityParts.push(`${metrics.quality.risks} risk(s)`);
|
|
15069
|
+
if (metrics.quality.openQuestions > 0)
|
|
15070
|
+
qualityParts.push(`${metrics.quality.openQuestions} open question(s)`);
|
|
15071
|
+
areas.push({
|
|
15072
|
+
name: "Quality",
|
|
15073
|
+
status: qualityStatus,
|
|
15074
|
+
summary: qualityParts.length > 0 ? qualityParts.join(", ") : "no issues",
|
|
15075
|
+
items: metrics.quality.items
|
|
15076
|
+
});
|
|
15077
|
+
const resourceCount = metrics.resources.unowned;
|
|
15078
|
+
const resourceStatus = resourceCount === 0 ? "green" : resourceCount <= 2 ? "amber" : "red";
|
|
15079
|
+
areas.push({
|
|
15080
|
+
name: "Resources",
|
|
15081
|
+
status: resourceStatus,
|
|
15082
|
+
summary: resourceCount > 0 ? `${resourceCount} unowned action(s)` : "all assigned",
|
|
15083
|
+
items: metrics.resources.items
|
|
15084
|
+
});
|
|
15085
|
+
const overall = worstStatus(areas.map((a) => a.status));
|
|
15086
|
+
return {
|
|
15087
|
+
projectName,
|
|
15088
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
15089
|
+
overall,
|
|
15090
|
+
areas,
|
|
15091
|
+
metrics
|
|
15092
|
+
};
|
|
15093
|
+
}
|
|
15094
|
+
|
|
15095
|
+
// src/plugins/builtin/tools/reports.ts
|
|
14974
15096
|
function createReportTools(store) {
|
|
14975
15097
|
return [
|
|
14976
15098
|
tool8(
|
|
@@ -15059,41 +15181,10 @@ function createReportTools(store) {
|
|
|
15059
15181
|
"Generate a Green-Amber-Red report with metrics across scope, schedule, quality, and resources",
|
|
15060
15182
|
{},
|
|
15061
15183
|
async () => {
|
|
15062
|
-
const
|
|
15063
|
-
const
|
|
15064
|
-
const doneActions = allActions.filter((d) => d.frontmatter.status === "done");
|
|
15065
|
-
const allDocs = store.list();
|
|
15066
|
-
const blockedItems = allDocs.filter(
|
|
15067
|
-
(d) => d.frontmatter.tags?.includes("blocked")
|
|
15068
|
-
);
|
|
15069
|
-
const overdueItems = allDocs.filter(
|
|
15070
|
-
(d) => d.frontmatter.tags?.includes("overdue")
|
|
15071
|
-
);
|
|
15072
|
-
const openQuestions = store.list({ type: "question", status: "open" });
|
|
15073
|
-
const riskItems = allDocs.filter(
|
|
15074
|
-
(d) => d.frontmatter.tags?.includes("risk")
|
|
15075
|
-
);
|
|
15076
|
-
const unownedActions = openActions.filter((d) => !d.frontmatter.owner);
|
|
15077
|
-
const areas = {
|
|
15078
|
-
scope: {
|
|
15079
|
-
total: allActions.length,
|
|
15080
|
-
open: openActions.length,
|
|
15081
|
-
done: doneActions.length
|
|
15082
|
-
},
|
|
15083
|
-
schedule: {
|
|
15084
|
-
blocked: blockedItems.length,
|
|
15085
|
-
overdue: overdueItems.length
|
|
15086
|
-
},
|
|
15087
|
-
quality: {
|
|
15088
|
-
openQuestions: openQuestions.length,
|
|
15089
|
-
risks: riskItems.length
|
|
15090
|
-
},
|
|
15091
|
-
resources: {
|
|
15092
|
-
unowned: unownedActions.length
|
|
15093
|
-
}
|
|
15094
|
-
};
|
|
15184
|
+
const metrics = collectGarMetrics(store);
|
|
15185
|
+
const report = evaluateGar("project", metrics);
|
|
15095
15186
|
return {
|
|
15096
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
15187
|
+
content: [{ type: "text", text: JSON.stringify(report, null, 2) }]
|
|
15097
15188
|
};
|
|
15098
15189
|
},
|
|
15099
15190
|
{ annotations: { readOnly: true } }
|
|
@@ -15873,6 +15964,188 @@ function createSprintTools(store) {
|
|
|
15873
15964
|
];
|
|
15874
15965
|
}
|
|
15875
15966
|
|
|
15967
|
+
// src/plugins/builtin/tools/sprint-planning.ts
|
|
15968
|
+
import { tool as tool13 } from "@anthropic-ai/claude-agent-sdk";
|
|
15969
|
+
var PRIORITY_ORDER = {
|
|
15970
|
+
critical: 0,
|
|
15971
|
+
high: 1,
|
|
15972
|
+
medium: 2,
|
|
15973
|
+
low: 3
|
|
15974
|
+
};
|
|
15975
|
+
function priorityRank(p) {
|
|
15976
|
+
return PRIORITY_ORDER[p ?? ""] ?? 99;
|
|
15977
|
+
}
|
|
15978
|
+
function createSprintPlanningTools(store) {
|
|
15979
|
+
return [
|
|
15980
|
+
tool13(
|
|
15981
|
+
"gather_sprint_planning_context",
|
|
15982
|
+
"Aggregate all planning-relevant data for proposing the next sprint: approved features, backlog epics, active sprint, velocity reference, blockers, and summary stats",
|
|
15983
|
+
{
|
|
15984
|
+
focusFeature: external_exports.string().optional().describe("Filter backlog to epics of a specific feature ID (e.g. 'F-001')"),
|
|
15985
|
+
sprintDurationDays: external_exports.number().optional().describe("Expected sprint duration in days \u2014 passed through for capacity reasoning")
|
|
15986
|
+
},
|
|
15987
|
+
async (args) => {
|
|
15988
|
+
const features = store.list({ type: "feature" });
|
|
15989
|
+
const epics = store.list({ type: "epic" });
|
|
15990
|
+
const sprints = store.list({ type: "sprint" });
|
|
15991
|
+
const questions = store.list({ type: "question", status: "open" });
|
|
15992
|
+
const contributions = store.list({ type: "contribution" });
|
|
15993
|
+
const approvedFeatures = features.filter((f) => f.frontmatter.status === "approved").sort((a, b) => priorityRank(a.frontmatter.priority) - priorityRank(b.frontmatter.priority)).map((f) => {
|
|
15994
|
+
const linkedEpics = epics.filter((e) => e.frontmatter.linkedFeature === f.frontmatter.id);
|
|
15995
|
+
const epicsByStatus = {};
|
|
15996
|
+
for (const e of linkedEpics) {
|
|
15997
|
+
epicsByStatus[e.frontmatter.status] = (epicsByStatus[e.frontmatter.status] ?? 0) + 1;
|
|
15998
|
+
}
|
|
15999
|
+
return {
|
|
16000
|
+
id: f.frontmatter.id,
|
|
16001
|
+
title: f.frontmatter.title,
|
|
16002
|
+
priority: f.frontmatter.priority,
|
|
16003
|
+
owner: f.frontmatter.owner,
|
|
16004
|
+
epicCount: linkedEpics.length,
|
|
16005
|
+
epicsByStatus
|
|
16006
|
+
};
|
|
16007
|
+
});
|
|
16008
|
+
const assignedEpicIds = /* @__PURE__ */ new Set();
|
|
16009
|
+
for (const sp of sprints) {
|
|
16010
|
+
const linked = sp.frontmatter.linkedEpics ?? [];
|
|
16011
|
+
for (const id of linked) assignedEpicIds.add(id);
|
|
16012
|
+
}
|
|
16013
|
+
const featureMap = new Map(features.map((f) => [f.frontmatter.id, f]));
|
|
16014
|
+
let backlogEpics = epics.filter(
|
|
16015
|
+
(e) => !assignedEpicIds.has(e.frontmatter.id) && e.frontmatter.status !== "done"
|
|
16016
|
+
);
|
|
16017
|
+
if (args.focusFeature) {
|
|
16018
|
+
backlogEpics = backlogEpics.filter(
|
|
16019
|
+
(e) => e.frontmatter.linkedFeature === args.focusFeature
|
|
16020
|
+
);
|
|
16021
|
+
}
|
|
16022
|
+
const backlog = backlogEpics.sort((a, b) => {
|
|
16023
|
+
const fa = featureMap.get(a.frontmatter.linkedFeature);
|
|
16024
|
+
const fb = featureMap.get(b.frontmatter.linkedFeature);
|
|
16025
|
+
return priorityRank(fa?.frontmatter.priority) - priorityRank(fb?.frontmatter.priority);
|
|
16026
|
+
}).map((e) => {
|
|
16027
|
+
const parent = featureMap.get(e.frontmatter.linkedFeature);
|
|
16028
|
+
return {
|
|
16029
|
+
id: e.frontmatter.id,
|
|
16030
|
+
title: e.frontmatter.title,
|
|
16031
|
+
status: e.frontmatter.status,
|
|
16032
|
+
linkedFeature: e.frontmatter.linkedFeature,
|
|
16033
|
+
featureTitle: parent?.frontmatter.title ?? null,
|
|
16034
|
+
featurePriority: parent?.frontmatter.priority ?? null,
|
|
16035
|
+
estimatedEffort: e.frontmatter.estimatedEffort ?? null,
|
|
16036
|
+
targetDate: e.frontmatter.targetDate ?? null
|
|
16037
|
+
};
|
|
16038
|
+
});
|
|
16039
|
+
const activeSprintDoc = sprints.find((s) => s.frontmatter.status === "active") ?? null;
|
|
16040
|
+
let activeSprint = null;
|
|
16041
|
+
if (activeSprintDoc) {
|
|
16042
|
+
const linkedEpicIds = activeSprintDoc.frontmatter.linkedEpics ?? [];
|
|
16043
|
+
const linkedEpics = linkedEpicIds.map((epicId) => {
|
|
16044
|
+
const epic = store.get(epicId);
|
|
16045
|
+
return epic ? { id: epicId, title: epic.frontmatter.title, status: epic.frontmatter.status } : { id: epicId, title: "(not found)", status: "unknown" };
|
|
16046
|
+
});
|
|
16047
|
+
const allDocs = store.list();
|
|
16048
|
+
const sprintTag = `sprint:${activeSprintDoc.frontmatter.id}`;
|
|
16049
|
+
const workItems = allDocs.filter(
|
|
16050
|
+
(d) => d.frontmatter.type !== "sprint" && d.frontmatter.type !== "epic" && d.frontmatter.tags?.includes(sprintTag)
|
|
16051
|
+
);
|
|
16052
|
+
const doneCount = workItems.filter(
|
|
16053
|
+
(d) => d.frontmatter.status === "done" || d.frontmatter.status === "resolved" || d.frontmatter.status === "closed"
|
|
16054
|
+
).length;
|
|
16055
|
+
const completionPct = workItems.length > 0 ? Math.round(doneCount / workItems.length * 100) : 0;
|
|
16056
|
+
activeSprint = {
|
|
16057
|
+
id: activeSprintDoc.frontmatter.id,
|
|
16058
|
+
title: activeSprintDoc.frontmatter.title,
|
|
16059
|
+
goal: activeSprintDoc.frontmatter.goal,
|
|
16060
|
+
startDate: activeSprintDoc.frontmatter.startDate,
|
|
16061
|
+
endDate: activeSprintDoc.frontmatter.endDate,
|
|
16062
|
+
linkedEpics,
|
|
16063
|
+
workItems: { total: workItems.length, done: doneCount, completionPct }
|
|
16064
|
+
};
|
|
16065
|
+
}
|
|
16066
|
+
const completedSprints = sprints.filter((s) => s.frontmatter.status === "completed").sort((a, b) => {
|
|
16067
|
+
const da = a.frontmatter.endDate ?? "";
|
|
16068
|
+
const db = b.frontmatter.endDate ?? "";
|
|
16069
|
+
return da < db ? 1 : da > db ? -1 : 0;
|
|
16070
|
+
}).slice(0, 2);
|
|
16071
|
+
const velocityReference = completedSprints.map((sp) => {
|
|
16072
|
+
const linkedEpicIds = sp.frontmatter.linkedEpics ?? [];
|
|
16073
|
+
const efforts = [];
|
|
16074
|
+
for (const epicId of linkedEpicIds) {
|
|
16075
|
+
const epic = store.get(epicId);
|
|
16076
|
+
if (epic?.frontmatter.estimatedEffort) {
|
|
16077
|
+
efforts.push(String(epic.frontmatter.estimatedEffort));
|
|
16078
|
+
}
|
|
16079
|
+
}
|
|
16080
|
+
const allDocs = store.list();
|
|
16081
|
+
const sprintTag = `sprint:${sp.frontmatter.id}`;
|
|
16082
|
+
const workItems = allDocs.filter(
|
|
16083
|
+
(d) => d.frontmatter.type !== "sprint" && d.frontmatter.type !== "epic" && d.frontmatter.tags?.includes(sprintTag)
|
|
16084
|
+
);
|
|
16085
|
+
return {
|
|
16086
|
+
id: sp.frontmatter.id,
|
|
16087
|
+
title: sp.frontmatter.title,
|
|
16088
|
+
startDate: sp.frontmatter.startDate,
|
|
16089
|
+
endDate: sp.frontmatter.endDate,
|
|
16090
|
+
epicCount: linkedEpicIds.length,
|
|
16091
|
+
efforts,
|
|
16092
|
+
workItemCount: workItems.length
|
|
16093
|
+
};
|
|
16094
|
+
});
|
|
16095
|
+
const openBlockerContributions = contributions.filter(
|
|
16096
|
+
(c) => c.frontmatter.status === "open" && (c.frontmatter.contributionType === "risk-finding" || c.frontmatter.contributionType === "blocker-report")
|
|
16097
|
+
);
|
|
16098
|
+
const blockers = {
|
|
16099
|
+
openQuestions: questions.map((q) => ({
|
|
16100
|
+
id: q.frontmatter.id,
|
|
16101
|
+
title: q.frontmatter.title
|
|
16102
|
+
})),
|
|
16103
|
+
openRiskAndBlockerContributions: openBlockerContributions.map((c) => ({
|
|
16104
|
+
id: c.frontmatter.id,
|
|
16105
|
+
title: c.frontmatter.title,
|
|
16106
|
+
contributionType: c.frontmatter.contributionType
|
|
16107
|
+
}))
|
|
16108
|
+
};
|
|
16109
|
+
const totalBacklogEfforts = backlog.filter((e) => e.estimatedEffort !== null).map((e) => String(e.estimatedEffort));
|
|
16110
|
+
const approvedFeaturesWithNoEpics = approvedFeatures.filter((f) => f.epicCount === 0).map((f) => ({ id: f.id, title: f.title }));
|
|
16111
|
+
const now = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
16112
|
+
const epicsAtRisk = epics.filter((e) => {
|
|
16113
|
+
if (e.frontmatter.status === "done") return false;
|
|
16114
|
+
if (e.frontmatter.targetDate && e.frontmatter.targetDate < now) return true;
|
|
16115
|
+
const parent = featureMap.get(e.frontmatter.linkedFeature);
|
|
16116
|
+
if (parent?.frontmatter.status === "deferred") return true;
|
|
16117
|
+
return false;
|
|
16118
|
+
}).map((e) => ({
|
|
16119
|
+
id: e.frontmatter.id,
|
|
16120
|
+
title: e.frontmatter.title,
|
|
16121
|
+
reason: e.frontmatter.targetDate && e.frontmatter.targetDate < now ? "past-target-date" : "deferred-feature"
|
|
16122
|
+
}));
|
|
16123
|
+
const plannedSprintCount = sprints.filter((s) => s.frontmatter.status === "planned").length;
|
|
16124
|
+
const summary = {
|
|
16125
|
+
totalBacklogEpics: backlog.length,
|
|
16126
|
+
totalBacklogEfforts,
|
|
16127
|
+
approvedFeaturesWithNoEpics,
|
|
16128
|
+
epicsAtRisk,
|
|
16129
|
+
plannedSprintCount
|
|
16130
|
+
};
|
|
16131
|
+
const result = {
|
|
16132
|
+
approvedFeatures,
|
|
16133
|
+
backlog,
|
|
16134
|
+
activeSprint,
|
|
16135
|
+
velocityReference,
|
|
16136
|
+
blockers,
|
|
16137
|
+
summary,
|
|
16138
|
+
...args.sprintDurationDays !== void 0 ? { sprintDurationDays: args.sprintDurationDays } : {}
|
|
16139
|
+
};
|
|
16140
|
+
return {
|
|
16141
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
16142
|
+
};
|
|
16143
|
+
},
|
|
16144
|
+
{ annotations: { readOnly: true } }
|
|
16145
|
+
)
|
|
16146
|
+
];
|
|
16147
|
+
}
|
|
16148
|
+
|
|
15876
16149
|
// src/plugins/common.ts
|
|
15877
16150
|
var COMMON_REGISTRATIONS = [
|
|
15878
16151
|
{ type: "meeting", dirName: "meetings", idPrefix: "M" },
|
|
@@ -15889,7 +16162,8 @@ function createCommonTools(store) {
|
|
|
15889
16162
|
...createFeatureTools(store),
|
|
15890
16163
|
...createEpicTools(store),
|
|
15891
16164
|
...createContributionTools(store),
|
|
15892
|
-
...createSprintTools(store)
|
|
16165
|
+
...createSprintTools(store),
|
|
16166
|
+
...createSprintPlanningTools(store)
|
|
15893
16167
|
];
|
|
15894
16168
|
}
|
|
15895
16169
|
|
|
@@ -15962,7 +16236,12 @@ var genericAgilePlugin = {
|
|
|
15962
16236
|
- **list_sprints** / **get_sprint**: View sprints to understand iteration scope and delivery dates.
|
|
15963
16237
|
- **update_sprint**: Assign epics to sprints by updating linkedEpics when breaking features into work.
|
|
15964
16238
|
- Tag technical actions and decisions with \`sprint:SP-xxx\` to associate them with a sprint.
|
|
15965
|
-
- Use **generate_sprint_progress** to track technical work completion within an iteration
|
|
16239
|
+
- Use **generate_sprint_progress** to track technical work completion within an iteration.
|
|
16240
|
+
|
|
16241
|
+
**Sprint Planning:**
|
|
16242
|
+
- When asked to plan or propose a sprint, ALWAYS call **gather_sprint_planning_context** first.
|
|
16243
|
+
- Focus on: technical readiness of each epic, open technical questions or spikes, effort balance across the sprint, and feature coverage.
|
|
16244
|
+
- Present a structured sprint proposal with technical rationale for each selected epic, known technical risks, and any prerequisite work that should be completed first.`,
|
|
15966
16245
|
"delivery-manager": `You track delivery across features and epics, manage schedules, and report on progress.
|
|
15967
16246
|
|
|
15968
16247
|
**Report Tools:**
|
|
@@ -16010,7 +16289,13 @@ var genericAgilePlugin = {
|
|
|
16010
16289
|
- Assign epics to sprints via linkedEpics.
|
|
16011
16290
|
- Tag work items (actions, decisions, questions) with \`sprint:SP-xxx\` for sprint scoping.
|
|
16012
16291
|
- Track delivery dates and flag at-risk sprints.
|
|
16013
|
-
- Register past/completed sprints for historical tracking
|
|
16292
|
+
- Register past/completed sprints for historical tracking.
|
|
16293
|
+
|
|
16294
|
+
**Sprint Planning:**
|
|
16295
|
+
- When asked to plan or propose a sprint, ALWAYS call **gather_sprint_planning_context** first. It aggregates approved features, backlog epics, active sprint status, velocity from recent sprints, blockers, and summary stats in one call.
|
|
16296
|
+
- Reason through: priority (critical/high features first), capacity (compare backlog effort to velocity reference), dependencies and blockers, balance across features, and risk.
|
|
16297
|
+
- Present a structured sprint proposal: title, goal, suggested dates, selected epics with rationale for each, excluded epics with reason, and identified risks.
|
|
16298
|
+
- After user confirmation, use **create_sprint** with the agreed epics to persist the sprint.`,
|
|
16014
16299
|
"*": `You have access to feature, epic, sprint, and meeting tools for project coordination:
|
|
16015
16300
|
|
|
16016
16301
|
**Features** (F-xxx): Product capabilities defined by the Product Owner. Features progress through draft \u2192 approved \u2192 done.
|
|
@@ -16033,10 +16318,10 @@ var genericAgilePlugin = {
|
|
|
16033
16318
|
};
|
|
16034
16319
|
|
|
16035
16320
|
// src/plugins/builtin/tools/use-cases.ts
|
|
16036
|
-
import { tool as
|
|
16321
|
+
import { tool as tool14 } from "@anthropic-ai/claude-agent-sdk";
|
|
16037
16322
|
function createUseCaseTools(store) {
|
|
16038
16323
|
return [
|
|
16039
|
-
|
|
16324
|
+
tool14(
|
|
16040
16325
|
"list_use_cases",
|
|
16041
16326
|
"List all extension use cases, optionally filtered by status or extension type",
|
|
16042
16327
|
{
|
|
@@ -16066,7 +16351,7 @@ function createUseCaseTools(store) {
|
|
|
16066
16351
|
},
|
|
16067
16352
|
{ annotations: { readOnly: true } }
|
|
16068
16353
|
),
|
|
16069
|
-
|
|
16354
|
+
tool14(
|
|
16070
16355
|
"get_use_case",
|
|
16071
16356
|
"Get the full content of a specific use case by ID",
|
|
16072
16357
|
{ id: external_exports.string().describe("Use case ID (e.g. 'UC-001')") },
|
|
@@ -16093,7 +16378,7 @@ function createUseCaseTools(store) {
|
|
|
16093
16378
|
},
|
|
16094
16379
|
{ annotations: { readOnly: true } }
|
|
16095
16380
|
),
|
|
16096
|
-
|
|
16381
|
+
tool14(
|
|
16097
16382
|
"create_use_case",
|
|
16098
16383
|
"Create a new extension use case definition (Phase 1: Assess Extension Use Case)",
|
|
16099
16384
|
{
|
|
@@ -16127,7 +16412,7 @@ function createUseCaseTools(store) {
|
|
|
16127
16412
|
};
|
|
16128
16413
|
}
|
|
16129
16414
|
),
|
|
16130
|
-
|
|
16415
|
+
tool14(
|
|
16131
16416
|
"update_use_case",
|
|
16132
16417
|
"Update an existing extension use case",
|
|
16133
16418
|
{
|
|
@@ -16157,10 +16442,10 @@ function createUseCaseTools(store) {
|
|
|
16157
16442
|
}
|
|
16158
16443
|
|
|
16159
16444
|
// src/plugins/builtin/tools/tech-assessments.ts
|
|
16160
|
-
import { tool as
|
|
16445
|
+
import { tool as tool15 } from "@anthropic-ai/claude-agent-sdk";
|
|
16161
16446
|
function createTechAssessmentTools(store) {
|
|
16162
16447
|
return [
|
|
16163
|
-
|
|
16448
|
+
tool15(
|
|
16164
16449
|
"list_tech_assessments",
|
|
16165
16450
|
"List all technology assessments, optionally filtered by status",
|
|
16166
16451
|
{
|
|
@@ -16191,7 +16476,7 @@ function createTechAssessmentTools(store) {
|
|
|
16191
16476
|
},
|
|
16192
16477
|
{ annotations: { readOnly: true } }
|
|
16193
16478
|
),
|
|
16194
|
-
|
|
16479
|
+
tool15(
|
|
16195
16480
|
"get_tech_assessment",
|
|
16196
16481
|
"Get the full content of a specific technology assessment by ID",
|
|
16197
16482
|
{ id: external_exports.string().describe("Tech assessment ID (e.g. 'TA-001')") },
|
|
@@ -16218,7 +16503,7 @@ function createTechAssessmentTools(store) {
|
|
|
16218
16503
|
},
|
|
16219
16504
|
{ annotations: { readOnly: true } }
|
|
16220
16505
|
),
|
|
16221
|
-
|
|
16506
|
+
tool15(
|
|
16222
16507
|
"create_tech_assessment",
|
|
16223
16508
|
"Create a new technology assessment linked to an assessed/approved use case (Phase 2: Assess Extension Technology)",
|
|
16224
16509
|
{
|
|
@@ -16289,7 +16574,7 @@ function createTechAssessmentTools(store) {
|
|
|
16289
16574
|
};
|
|
16290
16575
|
}
|
|
16291
16576
|
),
|
|
16292
|
-
|
|
16577
|
+
tool15(
|
|
16293
16578
|
"update_tech_assessment",
|
|
16294
16579
|
"Update an existing technology assessment. The linked use case cannot be changed.",
|
|
16295
16580
|
{
|
|
@@ -16319,10 +16604,10 @@ function createTechAssessmentTools(store) {
|
|
|
16319
16604
|
}
|
|
16320
16605
|
|
|
16321
16606
|
// src/plugins/builtin/tools/extension-designs.ts
|
|
16322
|
-
import { tool as
|
|
16607
|
+
import { tool as tool16 } from "@anthropic-ai/claude-agent-sdk";
|
|
16323
16608
|
function createExtensionDesignTools(store) {
|
|
16324
16609
|
return [
|
|
16325
|
-
|
|
16610
|
+
tool16(
|
|
16326
16611
|
"list_extension_designs",
|
|
16327
16612
|
"List all extension designs, optionally filtered by status",
|
|
16328
16613
|
{
|
|
@@ -16352,7 +16637,7 @@ function createExtensionDesignTools(store) {
|
|
|
16352
16637
|
},
|
|
16353
16638
|
{ annotations: { readOnly: true } }
|
|
16354
16639
|
),
|
|
16355
|
-
|
|
16640
|
+
tool16(
|
|
16356
16641
|
"get_extension_design",
|
|
16357
16642
|
"Get the full content of a specific extension design by ID",
|
|
16358
16643
|
{ id: external_exports.string().describe("Extension design ID (e.g. 'XD-001')") },
|
|
@@ -16379,7 +16664,7 @@ function createExtensionDesignTools(store) {
|
|
|
16379
16664
|
},
|
|
16380
16665
|
{ annotations: { readOnly: true } }
|
|
16381
16666
|
),
|
|
16382
|
-
|
|
16667
|
+
tool16(
|
|
16383
16668
|
"create_extension_design",
|
|
16384
16669
|
"Create a new extension design linked to a recommended tech assessment (Phase 3: Define Extension Target Solution)",
|
|
16385
16670
|
{
|
|
@@ -16447,7 +16732,7 @@ function createExtensionDesignTools(store) {
|
|
|
16447
16732
|
};
|
|
16448
16733
|
}
|
|
16449
16734
|
),
|
|
16450
|
-
|
|
16735
|
+
tool16(
|
|
16451
16736
|
"update_extension_design",
|
|
16452
16737
|
"Update an existing extension design. The linked tech assessment cannot be changed.",
|
|
16453
16738
|
{
|
|
@@ -16476,10 +16761,10 @@ function createExtensionDesignTools(store) {
|
|
|
16476
16761
|
}
|
|
16477
16762
|
|
|
16478
16763
|
// src/plugins/builtin/tools/aem-reports.ts
|
|
16479
|
-
import { tool as
|
|
16764
|
+
import { tool as tool17 } from "@anthropic-ai/claude-agent-sdk";
|
|
16480
16765
|
function createAemReportTools(store) {
|
|
16481
16766
|
return [
|
|
16482
|
-
|
|
16767
|
+
tool17(
|
|
16483
16768
|
"generate_extension_portfolio",
|
|
16484
16769
|
"Generate a portfolio view of all use cases with their linked tech assessments and extension designs",
|
|
16485
16770
|
{},
|
|
@@ -16531,7 +16816,7 @@ function createAemReportTools(store) {
|
|
|
16531
16816
|
},
|
|
16532
16817
|
{ annotations: { readOnly: true } }
|
|
16533
16818
|
),
|
|
16534
|
-
|
|
16819
|
+
tool17(
|
|
16535
16820
|
"generate_tech_readiness",
|
|
16536
16821
|
"Generate a BTP technology readiness report showing service coverage and gaps across assessments",
|
|
16537
16822
|
{},
|
|
@@ -16583,7 +16868,7 @@ function createAemReportTools(store) {
|
|
|
16583
16868
|
},
|
|
16584
16869
|
{ annotations: { readOnly: true } }
|
|
16585
16870
|
),
|
|
16586
|
-
|
|
16871
|
+
tool17(
|
|
16587
16872
|
"generate_phase_status",
|
|
16588
16873
|
"Generate a phase progress report showing artifact counts and readiness per AEM phase",
|
|
16589
16874
|
{},
|
|
@@ -16645,11 +16930,11 @@ function createAemReportTools(store) {
|
|
|
16645
16930
|
import * as fs6 from "fs";
|
|
16646
16931
|
import * as path6 from "path";
|
|
16647
16932
|
import * as YAML4 from "yaml";
|
|
16648
|
-
import { tool as
|
|
16933
|
+
import { tool as tool18 } from "@anthropic-ai/claude-agent-sdk";
|
|
16649
16934
|
var PHASES = ["assess-use-case", "assess-technology", "define-solution"];
|
|
16650
16935
|
function createAemPhaseTools(store, marvinDir) {
|
|
16651
16936
|
return [
|
|
16652
|
-
|
|
16937
|
+
tool18(
|
|
16653
16938
|
"get_current_phase",
|
|
16654
16939
|
"Get the current AEM phase from project configuration",
|
|
16655
16940
|
{},
|
|
@@ -16670,7 +16955,7 @@ function createAemPhaseTools(store, marvinDir) {
|
|
|
16670
16955
|
},
|
|
16671
16956
|
{ annotations: { readOnly: true } }
|
|
16672
16957
|
),
|
|
16673
|
-
|
|
16958
|
+
tool18(
|
|
16674
16959
|
"advance_phase",
|
|
16675
16960
|
"Advance to the next AEM phase. Performs soft gate checks and warns if artifacts are incomplete, but does not block.",
|
|
16676
16961
|
{
|
|
@@ -16977,9 +17262,495 @@ Be thorough but concise. Focus on actionable insights.`,
|
|
|
16977
17262
|
]
|
|
16978
17263
|
};
|
|
16979
17264
|
|
|
17265
|
+
// src/skills/builtin/jira/tools.ts
|
|
17266
|
+
import { tool as tool19 } from "@anthropic-ai/claude-agent-sdk";
|
|
17267
|
+
|
|
17268
|
+
// src/skills/builtin/jira/client.ts
|
|
17269
|
+
var JiraClient = class {
|
|
17270
|
+
baseUrl;
|
|
17271
|
+
authHeader;
|
|
17272
|
+
constructor(config2) {
|
|
17273
|
+
this.baseUrl = `https://${config2.host}/rest/api/2`;
|
|
17274
|
+
this.authHeader = "Basic " + Buffer.from(`${config2.email}:${config2.apiToken}`).toString("base64");
|
|
17275
|
+
}
|
|
17276
|
+
async request(path10, method = "GET", body) {
|
|
17277
|
+
const url2 = `${this.baseUrl}${path10}`;
|
|
17278
|
+
const headers = {
|
|
17279
|
+
Authorization: this.authHeader,
|
|
17280
|
+
"Content-Type": "application/json",
|
|
17281
|
+
Accept: "application/json"
|
|
17282
|
+
};
|
|
17283
|
+
const response = await fetch(url2, {
|
|
17284
|
+
method,
|
|
17285
|
+
headers,
|
|
17286
|
+
body: body ? JSON.stringify(body) : void 0
|
|
17287
|
+
});
|
|
17288
|
+
if (!response.ok) {
|
|
17289
|
+
const text = await response.text().catch(() => "");
|
|
17290
|
+
throw new Error(
|
|
17291
|
+
`Jira API error ${response.status} ${method} ${path10}: ${text}`
|
|
17292
|
+
);
|
|
17293
|
+
}
|
|
17294
|
+
if (response.status === 204) return void 0;
|
|
17295
|
+
return response.json();
|
|
17296
|
+
}
|
|
17297
|
+
async searchIssues(jql, maxResults = 50) {
|
|
17298
|
+
const params = new URLSearchParams({
|
|
17299
|
+
jql,
|
|
17300
|
+
maxResults: String(maxResults)
|
|
17301
|
+
});
|
|
17302
|
+
return this.request(`/search?${params}`);
|
|
17303
|
+
}
|
|
17304
|
+
async getIssue(key) {
|
|
17305
|
+
return this.request(`/issue/${encodeURIComponent(key)}`);
|
|
17306
|
+
}
|
|
17307
|
+
async createIssue(fields) {
|
|
17308
|
+
return this.request("/issue", "POST", { fields });
|
|
17309
|
+
}
|
|
17310
|
+
async updateIssue(key, fields) {
|
|
17311
|
+
await this.request(
|
|
17312
|
+
`/issue/${encodeURIComponent(key)}`,
|
|
17313
|
+
"PUT",
|
|
17314
|
+
{ fields }
|
|
17315
|
+
);
|
|
17316
|
+
}
|
|
17317
|
+
async addComment(key, body) {
|
|
17318
|
+
await this.request(
|
|
17319
|
+
`/issue/${encodeURIComponent(key)}/comment`,
|
|
17320
|
+
"POST",
|
|
17321
|
+
{ body }
|
|
17322
|
+
);
|
|
17323
|
+
}
|
|
17324
|
+
};
|
|
17325
|
+
function createJiraClient() {
|
|
17326
|
+
const host = process.env.JIRA_HOST;
|
|
17327
|
+
const email3 = process.env.JIRA_EMAIL;
|
|
17328
|
+
const apiToken = process.env.JIRA_API_TOKEN;
|
|
17329
|
+
if (!host || !email3 || !apiToken) return null;
|
|
17330
|
+
return new JiraClient({ host, email: email3, apiToken });
|
|
17331
|
+
}
|
|
17332
|
+
|
|
17333
|
+
// src/skills/builtin/jira/tools.ts
|
|
17334
|
+
var JIRA_TYPE = "jira-issue";
|
|
17335
|
+
function jiraNotConfiguredError() {
|
|
17336
|
+
return {
|
|
17337
|
+
content: [
|
|
17338
|
+
{
|
|
17339
|
+
type: "text",
|
|
17340
|
+
text: "Jira is not configured. Set JIRA_HOST, JIRA_EMAIL, and JIRA_API_TOKEN environment variables."
|
|
17341
|
+
}
|
|
17342
|
+
],
|
|
17343
|
+
isError: true
|
|
17344
|
+
};
|
|
17345
|
+
}
|
|
17346
|
+
function mapJiraStatus(jiraStatus) {
|
|
17347
|
+
const lower = jiraStatus.toLowerCase();
|
|
17348
|
+
if (lower === "done" || lower === "closed" || lower === "resolved") return "done";
|
|
17349
|
+
if (lower === "in progress" || lower === "in review") return "in-progress";
|
|
17350
|
+
return "open";
|
|
17351
|
+
}
|
|
17352
|
+
function jiraIssueToFrontmatter(issue2, host, linkedArtifacts) {
|
|
17353
|
+
return {
|
|
17354
|
+
title: issue2.fields.summary,
|
|
17355
|
+
status: mapJiraStatus(issue2.fields.status.name),
|
|
17356
|
+
jiraKey: issue2.key,
|
|
17357
|
+
jiraUrl: `https://${host}/browse/${issue2.key}`,
|
|
17358
|
+
issueType: issue2.fields.issuetype.name,
|
|
17359
|
+
priority: issue2.fields.priority?.name ?? "None",
|
|
17360
|
+
assignee: issue2.fields.assignee?.displayName ?? "",
|
|
17361
|
+
labels: issue2.fields.labels ?? [],
|
|
17362
|
+
linkedArtifacts: linkedArtifacts ?? [],
|
|
17363
|
+
tags: [`jira:${issue2.key}`],
|
|
17364
|
+
lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
17365
|
+
};
|
|
17366
|
+
}
|
|
17367
|
+
function findByJiraKey(store, jiraKey) {
|
|
17368
|
+
const docs = store.list({ type: JIRA_TYPE });
|
|
17369
|
+
return docs.find((d) => d.frontmatter.jiraKey === jiraKey);
|
|
17370
|
+
}
|
|
17371
|
+
function createJiraTools(store) {
|
|
17372
|
+
return [
|
|
17373
|
+
// --- Local read tools ---
|
|
17374
|
+
tool19(
|
|
17375
|
+
"list_jira_issues",
|
|
17376
|
+
"List locally synced Jira issues (JI-xxx documents), optionally filtered by status or Jira key",
|
|
17377
|
+
{
|
|
17378
|
+
status: external_exports.enum(["open", "in-progress", "done"]).optional().describe("Filter by local status"),
|
|
17379
|
+
jiraKey: external_exports.string().optional().describe("Filter by Jira issue key (e.g. 'PROJ-123')")
|
|
17380
|
+
},
|
|
17381
|
+
async (args) => {
|
|
17382
|
+
let docs = store.list({ type: JIRA_TYPE, status: args.status });
|
|
17383
|
+
if (args.jiraKey) {
|
|
17384
|
+
docs = docs.filter((d) => d.frontmatter.jiraKey === args.jiraKey);
|
|
17385
|
+
}
|
|
17386
|
+
const summary = docs.map((d) => ({
|
|
17387
|
+
id: d.frontmatter.id,
|
|
17388
|
+
title: d.frontmatter.title,
|
|
17389
|
+
status: d.frontmatter.status,
|
|
17390
|
+
jiraKey: d.frontmatter.jiraKey,
|
|
17391
|
+
issueType: d.frontmatter.issueType,
|
|
17392
|
+
priority: d.frontmatter.priority,
|
|
17393
|
+
assignee: d.frontmatter.assignee,
|
|
17394
|
+
linkedArtifacts: d.frontmatter.linkedArtifacts
|
|
17395
|
+
}));
|
|
17396
|
+
return {
|
|
17397
|
+
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
17398
|
+
};
|
|
17399
|
+
},
|
|
17400
|
+
{ annotations: { readOnly: true } }
|
|
17401
|
+
),
|
|
17402
|
+
tool19(
|
|
17403
|
+
"get_jira_issue",
|
|
17404
|
+
"Get the full content of a locally synced Jira issue by local ID (JI-xxx) or Jira key (PROJ-123)",
|
|
17405
|
+
{
|
|
17406
|
+
id: external_exports.string().describe("Local ID (e.g. 'JI-001') or Jira key (e.g. 'PROJ-123')")
|
|
17407
|
+
},
|
|
17408
|
+
async (args) => {
|
|
17409
|
+
let doc = store.get(args.id);
|
|
17410
|
+
if (!doc) {
|
|
17411
|
+
doc = findByJiraKey(store, args.id);
|
|
17412
|
+
}
|
|
17413
|
+
if (!doc) {
|
|
17414
|
+
return {
|
|
17415
|
+
content: [{ type: "text", text: `Jira issue ${args.id} not found locally` }],
|
|
17416
|
+
isError: true
|
|
17417
|
+
};
|
|
17418
|
+
}
|
|
17419
|
+
return {
|
|
17420
|
+
content: [
|
|
17421
|
+
{
|
|
17422
|
+
type: "text",
|
|
17423
|
+
text: JSON.stringify(
|
|
17424
|
+
{ ...doc.frontmatter, content: doc.content },
|
|
17425
|
+
null,
|
|
17426
|
+
2
|
|
17427
|
+
)
|
|
17428
|
+
}
|
|
17429
|
+
]
|
|
17430
|
+
};
|
|
17431
|
+
},
|
|
17432
|
+
{ annotations: { readOnly: true } }
|
|
17433
|
+
),
|
|
17434
|
+
// --- Jira → Local tools ---
|
|
17435
|
+
tool19(
|
|
17436
|
+
"pull_jira_issue",
|
|
17437
|
+
"Fetch a single Jira issue by key and create/update a local JI-xxx document",
|
|
17438
|
+
{
|
|
17439
|
+
key: external_exports.string().describe("Jira issue key (e.g. 'PROJ-123')")
|
|
17440
|
+
},
|
|
17441
|
+
async (args) => {
|
|
17442
|
+
const client = createJiraClient();
|
|
17443
|
+
if (!client) return jiraNotConfiguredError();
|
|
17444
|
+
const issue2 = await client.getIssue(args.key);
|
|
17445
|
+
const host = process.env.JIRA_HOST;
|
|
17446
|
+
const existing = findByJiraKey(store, args.key);
|
|
17447
|
+
if (existing) {
|
|
17448
|
+
const fm2 = jiraIssueToFrontmatter(
|
|
17449
|
+
issue2,
|
|
17450
|
+
host,
|
|
17451
|
+
existing.frontmatter.linkedArtifacts
|
|
17452
|
+
);
|
|
17453
|
+
const doc2 = store.update(
|
|
17454
|
+
existing.frontmatter.id,
|
|
17455
|
+
fm2,
|
|
17456
|
+
issue2.fields.description ?? ""
|
|
17457
|
+
);
|
|
17458
|
+
return {
|
|
17459
|
+
content: [
|
|
17460
|
+
{
|
|
17461
|
+
type: "text",
|
|
17462
|
+
text: `Updated ${doc2.frontmatter.id} from Jira ${args.key}`
|
|
17463
|
+
}
|
|
17464
|
+
]
|
|
17465
|
+
};
|
|
17466
|
+
}
|
|
17467
|
+
const fm = jiraIssueToFrontmatter(issue2, host);
|
|
17468
|
+
const doc = store.create(
|
|
17469
|
+
JIRA_TYPE,
|
|
17470
|
+
fm,
|
|
17471
|
+
issue2.fields.description ?? ""
|
|
17472
|
+
);
|
|
17473
|
+
return {
|
|
17474
|
+
content: [
|
|
17475
|
+
{
|
|
17476
|
+
type: "text",
|
|
17477
|
+
text: `Created ${doc.frontmatter.id} from Jira ${args.key}`
|
|
17478
|
+
}
|
|
17479
|
+
]
|
|
17480
|
+
};
|
|
17481
|
+
}
|
|
17482
|
+
),
|
|
17483
|
+
tool19(
|
|
17484
|
+
"pull_jira_issues_jql",
|
|
17485
|
+
"Bulk fetch Jira issues via JQL query and create/update local JI-xxx documents",
|
|
17486
|
+
{
|
|
17487
|
+
jql: external_exports.string().describe(`JQL query (e.g. 'project = PROJ AND status = "In Progress"')`),
|
|
17488
|
+
maxResults: external_exports.number().optional().describe("Max issues to fetch (default 50)")
|
|
17489
|
+
},
|
|
17490
|
+
async (args) => {
|
|
17491
|
+
const client = createJiraClient();
|
|
17492
|
+
if (!client) return jiraNotConfiguredError();
|
|
17493
|
+
const result = await client.searchIssues(args.jql, args.maxResults);
|
|
17494
|
+
const host = process.env.JIRA_HOST;
|
|
17495
|
+
const created = [];
|
|
17496
|
+
const updated = [];
|
|
17497
|
+
for (const issue2 of result.issues) {
|
|
17498
|
+
const existing = findByJiraKey(store, issue2.key);
|
|
17499
|
+
if (existing) {
|
|
17500
|
+
const fm = jiraIssueToFrontmatter(
|
|
17501
|
+
issue2,
|
|
17502
|
+
host,
|
|
17503
|
+
existing.frontmatter.linkedArtifacts
|
|
17504
|
+
);
|
|
17505
|
+
store.update(
|
|
17506
|
+
existing.frontmatter.id,
|
|
17507
|
+
fm,
|
|
17508
|
+
issue2.fields.description ?? ""
|
|
17509
|
+
);
|
|
17510
|
+
updated.push(`${existing.frontmatter.id} (${issue2.key})`);
|
|
17511
|
+
} else {
|
|
17512
|
+
const fm = jiraIssueToFrontmatter(issue2, host);
|
|
17513
|
+
const doc = store.create(
|
|
17514
|
+
JIRA_TYPE,
|
|
17515
|
+
fm,
|
|
17516
|
+
issue2.fields.description ?? ""
|
|
17517
|
+
);
|
|
17518
|
+
created.push(`${doc.frontmatter.id} (${issue2.key})`);
|
|
17519
|
+
}
|
|
17520
|
+
}
|
|
17521
|
+
const parts = [
|
|
17522
|
+
`Fetched ${result.issues.length} of ${result.total} matching issues.`
|
|
17523
|
+
];
|
|
17524
|
+
if (created.length > 0) parts.push(`Created: ${created.join(", ")}`);
|
|
17525
|
+
if (updated.length > 0) parts.push(`Updated: ${updated.join(", ")}`);
|
|
17526
|
+
return {
|
|
17527
|
+
content: [{ type: "text", text: parts.join("\n") }]
|
|
17528
|
+
};
|
|
17529
|
+
}
|
|
17530
|
+
),
|
|
17531
|
+
// --- Local → Jira tools ---
|
|
17532
|
+
tool19(
|
|
17533
|
+
"push_artifact_to_jira",
|
|
17534
|
+
"Create a Jira issue from any Marvin artifact (D/A/Q/F/E) and create a tracking JI-xxx document",
|
|
17535
|
+
{
|
|
17536
|
+
artifactId: external_exports.string().describe("Marvin artifact ID (e.g. 'D-001', 'F-003', 'E-002')"),
|
|
17537
|
+
projectKey: external_exports.string().describe("Jira project key (e.g. 'PROJ')"),
|
|
17538
|
+
issueType: external_exports.enum(["Story", "Task", "Bug", "Epic"]).optional().describe("Jira issue type (default: 'Task')")
|
|
17539
|
+
},
|
|
17540
|
+
async (args) => {
|
|
17541
|
+
const client = createJiraClient();
|
|
17542
|
+
if (!client) return jiraNotConfiguredError();
|
|
17543
|
+
const artifact = store.get(args.artifactId);
|
|
17544
|
+
if (!artifact) {
|
|
17545
|
+
return {
|
|
17546
|
+
content: [
|
|
17547
|
+
{ type: "text", text: `Artifact ${args.artifactId} not found` }
|
|
17548
|
+
],
|
|
17549
|
+
isError: true
|
|
17550
|
+
};
|
|
17551
|
+
}
|
|
17552
|
+
const description = [
|
|
17553
|
+
artifact.content,
|
|
17554
|
+
"",
|
|
17555
|
+
`---`,
|
|
17556
|
+
`Marvin artifact: ${artifact.frontmatter.id} (${artifact.frontmatter.type})`,
|
|
17557
|
+
`Status: ${artifact.frontmatter.status}`
|
|
17558
|
+
].join("\n");
|
|
17559
|
+
const jiraResult = await client.createIssue({
|
|
17560
|
+
project: { key: args.projectKey },
|
|
17561
|
+
summary: artifact.frontmatter.title,
|
|
17562
|
+
description,
|
|
17563
|
+
issuetype: { name: args.issueType ?? "Task" }
|
|
17564
|
+
});
|
|
17565
|
+
const host = process.env.JIRA_HOST;
|
|
17566
|
+
const jiDoc = store.create(
|
|
17567
|
+
JIRA_TYPE,
|
|
17568
|
+
{
|
|
17569
|
+
title: artifact.frontmatter.title,
|
|
17570
|
+
status: "open",
|
|
17571
|
+
jiraKey: jiraResult.key,
|
|
17572
|
+
jiraUrl: `https://${host}/browse/${jiraResult.key}`,
|
|
17573
|
+
issueType: args.issueType ?? "Task",
|
|
17574
|
+
priority: "Medium",
|
|
17575
|
+
assignee: "",
|
|
17576
|
+
labels: [],
|
|
17577
|
+
linkedArtifacts: [args.artifactId],
|
|
17578
|
+
tags: [`jira:${jiraResult.key}`],
|
|
17579
|
+
lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
17580
|
+
},
|
|
17581
|
+
""
|
|
17582
|
+
);
|
|
17583
|
+
return {
|
|
17584
|
+
content: [
|
|
17585
|
+
{
|
|
17586
|
+
type: "text",
|
|
17587
|
+
text: `Created Jira ${jiraResult.key} from ${args.artifactId}. Tracking locally as ${jiDoc.frontmatter.id}.`
|
|
17588
|
+
}
|
|
17589
|
+
]
|
|
17590
|
+
};
|
|
17591
|
+
}
|
|
17592
|
+
),
|
|
17593
|
+
// --- Bidirectional sync ---
|
|
17594
|
+
tool19(
|
|
17595
|
+
"sync_jira_issue",
|
|
17596
|
+
"Bidirectional sync: push local title/description to Jira, pull latest status/assignee/labels back",
|
|
17597
|
+
{
|
|
17598
|
+
id: external_exports.string().describe("Local JI-xxx ID")
|
|
17599
|
+
},
|
|
17600
|
+
async (args) => {
|
|
17601
|
+
const client = createJiraClient();
|
|
17602
|
+
if (!client) return jiraNotConfiguredError();
|
|
17603
|
+
const doc = store.get(args.id);
|
|
17604
|
+
if (!doc || doc.frontmatter.type !== JIRA_TYPE) {
|
|
17605
|
+
return {
|
|
17606
|
+
content: [
|
|
17607
|
+
{ type: "text", text: `Jira issue ${args.id} not found locally` }
|
|
17608
|
+
],
|
|
17609
|
+
isError: true
|
|
17610
|
+
};
|
|
17611
|
+
}
|
|
17612
|
+
const jiraKey = doc.frontmatter.jiraKey;
|
|
17613
|
+
await client.updateIssue(jiraKey, {
|
|
17614
|
+
summary: doc.frontmatter.title,
|
|
17615
|
+
description: doc.content || void 0
|
|
17616
|
+
});
|
|
17617
|
+
const issue2 = await client.getIssue(jiraKey);
|
|
17618
|
+
const host = process.env.JIRA_HOST;
|
|
17619
|
+
const fm = jiraIssueToFrontmatter(
|
|
17620
|
+
issue2,
|
|
17621
|
+
host,
|
|
17622
|
+
doc.frontmatter.linkedArtifacts
|
|
17623
|
+
);
|
|
17624
|
+
store.update(args.id, fm, issue2.fields.description ?? "");
|
|
17625
|
+
return {
|
|
17626
|
+
content: [
|
|
17627
|
+
{
|
|
17628
|
+
type: "text",
|
|
17629
|
+
text: `Synced ${args.id} \u2194 ${jiraKey}. Status: ${fm.status}, Assignee: ${fm.assignee || "unassigned"}`
|
|
17630
|
+
}
|
|
17631
|
+
]
|
|
17632
|
+
};
|
|
17633
|
+
}
|
|
17634
|
+
),
|
|
17635
|
+
// --- Local link tool ---
|
|
17636
|
+
tool19(
|
|
17637
|
+
"link_artifact_to_jira",
|
|
17638
|
+
"Add a Marvin artifact ID to a JI-xxx document's linkedArtifacts field",
|
|
17639
|
+
{
|
|
17640
|
+
jiraIssueId: external_exports.string().describe("Local JI-xxx ID"),
|
|
17641
|
+
artifactId: external_exports.string().describe("Marvin artifact ID to link (e.g. 'D-001', 'F-003')")
|
|
17642
|
+
},
|
|
17643
|
+
async (args) => {
|
|
17644
|
+
const doc = store.get(args.jiraIssueId);
|
|
17645
|
+
if (!doc || doc.frontmatter.type !== JIRA_TYPE) {
|
|
17646
|
+
return {
|
|
17647
|
+
content: [
|
|
17648
|
+
{
|
|
17649
|
+
type: "text",
|
|
17650
|
+
text: `Jira issue ${args.jiraIssueId} not found locally`
|
|
17651
|
+
}
|
|
17652
|
+
],
|
|
17653
|
+
isError: true
|
|
17654
|
+
};
|
|
17655
|
+
}
|
|
17656
|
+
const artifact = store.get(args.artifactId);
|
|
17657
|
+
if (!artifact) {
|
|
17658
|
+
return {
|
|
17659
|
+
content: [
|
|
17660
|
+
{ type: "text", text: `Artifact ${args.artifactId} not found` }
|
|
17661
|
+
],
|
|
17662
|
+
isError: true
|
|
17663
|
+
};
|
|
17664
|
+
}
|
|
17665
|
+
const linked = doc.frontmatter.linkedArtifacts ?? [];
|
|
17666
|
+
if (linked.includes(args.artifactId)) {
|
|
17667
|
+
return {
|
|
17668
|
+
content: [
|
|
17669
|
+
{
|
|
17670
|
+
type: "text",
|
|
17671
|
+
text: `${args.artifactId} is already linked to ${args.jiraIssueId}`
|
|
17672
|
+
}
|
|
17673
|
+
]
|
|
17674
|
+
};
|
|
17675
|
+
}
|
|
17676
|
+
store.update(args.jiraIssueId, {
|
|
17677
|
+
linkedArtifacts: [...linked, args.artifactId]
|
|
17678
|
+
});
|
|
17679
|
+
return {
|
|
17680
|
+
content: [
|
|
17681
|
+
{
|
|
17682
|
+
type: "text",
|
|
17683
|
+
text: `Linked ${args.artifactId} to ${args.jiraIssueId}`
|
|
17684
|
+
}
|
|
17685
|
+
]
|
|
17686
|
+
};
|
|
17687
|
+
}
|
|
17688
|
+
)
|
|
17689
|
+
];
|
|
17690
|
+
}
|
|
17691
|
+
|
|
17692
|
+
// src/skills/builtin/jira/index.ts
|
|
17693
|
+
var jiraSkill = {
|
|
17694
|
+
id: "jira",
|
|
17695
|
+
name: "Jira Integration",
|
|
17696
|
+
description: "Bidirectional sync between Marvin artifacts and Jira issues",
|
|
17697
|
+
version: "1.0.0",
|
|
17698
|
+
format: "builtin-ts",
|
|
17699
|
+
// No default persona affinity — opt-in via config.yaml skills section
|
|
17700
|
+
documentTypeRegistrations: [
|
|
17701
|
+
{ type: "jira-issue", dirName: "jira-issues", idPrefix: "JI" }
|
|
17702
|
+
],
|
|
17703
|
+
tools: (store) => createJiraTools(store),
|
|
17704
|
+
promptFragments: {
|
|
17705
|
+
"product-owner": `You have the **Jira Integration** skill. You can pull issues from Jira and push Marvin artifacts to Jira.
|
|
17706
|
+
|
|
17707
|
+
**Available tools:**
|
|
17708
|
+
- \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues
|
|
17709
|
+
- \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import issues from Jira by key or JQL query
|
|
17710
|
+
- \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, feature, etc.)
|
|
17711
|
+
- \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
|
|
17712
|
+
- \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx
|
|
17713
|
+
|
|
17714
|
+
**As Product Owner, use Jira integration to:**
|
|
17715
|
+
- Pull stakeholder-reported issues for triage and prioritization
|
|
17716
|
+
- Push approved features as Stories for development tracking
|
|
17717
|
+
- Link decisions to Jira issues for audit trail and traceability
|
|
17718
|
+
- Use JQL queries to review backlog status (e.g. \`project = PROJ AND status = "To Do"\`)`,
|
|
17719
|
+
"tech-lead": `You have the **Jira Integration** skill. You can pull issues from Jira and push Marvin artifacts to Jira.
|
|
17720
|
+
|
|
17721
|
+
**Available tools:**
|
|
17722
|
+
- \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues
|
|
17723
|
+
- \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import issues from Jira by key or JQL query
|
|
17724
|
+
- \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, action, epic, etc.)
|
|
17725
|
+
- \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
|
|
17726
|
+
- \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx
|
|
17727
|
+
|
|
17728
|
+
**As Tech Lead, use Jira integration to:**
|
|
17729
|
+
- Pull technical issues and bugs for sprint planning and estimation
|
|
17730
|
+
- Push epics and technical decisions to Jira for cross-team visibility
|
|
17731
|
+
- Bidirectional sync to keep local governance and Jira in alignment
|
|
17732
|
+
- Use JQL queries to track technical debt (e.g. \`labels = "tech-debt" AND status != "Done"\`)`,
|
|
17733
|
+
"delivery-manager": `You have the **Jira Integration** skill. You can pull issues from Jira and push Marvin artifacts to Jira.
|
|
17734
|
+
|
|
17735
|
+
**Available tools:**
|
|
17736
|
+
- \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues
|
|
17737
|
+
- \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import issues from Jira by key or JQL query
|
|
17738
|
+
- \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, action, etc.)
|
|
17739
|
+
- \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
|
|
17740
|
+
- \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx
|
|
17741
|
+
|
|
17742
|
+
**As Delivery Manager, use Jira integration to:**
|
|
17743
|
+
- Pull sprint issues for tracking progress and blockers
|
|
17744
|
+
- Push actions and decisions to Jira for stakeholder visibility
|
|
17745
|
+
- Use JQL queries for reporting (e.g. \`sprint in openSprints() AND assignee = currentUser()\`)
|
|
17746
|
+
- Sync status between Marvin governance items and Jira issues`
|
|
17747
|
+
}
|
|
17748
|
+
};
|
|
17749
|
+
|
|
16980
17750
|
// src/skills/registry.ts
|
|
16981
17751
|
var BUILTIN_SKILLS = {
|
|
16982
|
-
"governance-review": governanceReviewSkill
|
|
17752
|
+
"governance-review": governanceReviewSkill,
|
|
17753
|
+
"jira": jiraSkill
|
|
16983
17754
|
};
|
|
16984
17755
|
function getBuiltinSkillsDir() {
|
|
16985
17756
|
const thisFile = fileURLToPath(import.meta.url);
|
|
@@ -17136,7 +17907,7 @@ ${fragment}`);
|
|
|
17136
17907
|
}
|
|
17137
17908
|
|
|
17138
17909
|
// src/skills/action-tools.ts
|
|
17139
|
-
import { tool as
|
|
17910
|
+
import { tool as tool20 } from "@anthropic-ai/claude-agent-sdk";
|
|
17140
17911
|
|
|
17141
17912
|
// src/skills/action-runner.ts
|
|
17142
17913
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
@@ -17226,7 +17997,7 @@ function createSkillActionTools(skills, context) {
|
|
|
17226
17997
|
if (!skill.actions) continue;
|
|
17227
17998
|
for (const action of skill.actions) {
|
|
17228
17999
|
tools.push(
|
|
17229
|
-
|
|
18000
|
+
tool20(
|
|
17230
18001
|
`${skill.id}__${action.id}`,
|
|
17231
18002
|
action.description,
|
|
17232
18003
|
{
|
|
@@ -17453,10 +18224,10 @@ ${lines.join("\n\n")}`;
|
|
|
17453
18224
|
}
|
|
17454
18225
|
|
|
17455
18226
|
// src/mcp/persona-tools.ts
|
|
17456
|
-
import { tool as
|
|
18227
|
+
import { tool as tool21 } from "@anthropic-ai/claude-agent-sdk";
|
|
17457
18228
|
function createPersonaTools(ctx, marvinDir) {
|
|
17458
18229
|
return [
|
|
17459
|
-
|
|
18230
|
+
tool21(
|
|
17460
18231
|
"set_persona",
|
|
17461
18232
|
"Set the active persona for this session. Returns full guidance for the selected persona including behavioral rules, allowed document types, and scope. Call this before working to ensure persona-appropriate behavior.",
|
|
17462
18233
|
{
|
|
@@ -17486,7 +18257,7 @@ ${summaries}`
|
|
|
17486
18257
|
};
|
|
17487
18258
|
}
|
|
17488
18259
|
),
|
|
17489
|
-
|
|
18260
|
+
tool21(
|
|
17490
18261
|
"get_persona_guidance",
|
|
17491
18262
|
"Get guidance for a persona without changing the active persona. If no persona is specified, lists all available personas with summaries.",
|
|
17492
18263
|
{
|