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/index.js
CHANGED
|
@@ -15302,6 +15302,128 @@ function createMeetingTools(store) {
|
|
|
15302
15302
|
|
|
15303
15303
|
// src/plugins/builtin/tools/reports.ts
|
|
15304
15304
|
import { tool as tool8 } from "@anthropic-ai/claude-agent-sdk";
|
|
15305
|
+
|
|
15306
|
+
// src/reports/gar/collector.ts
|
|
15307
|
+
function collectGarMetrics(store) {
|
|
15308
|
+
const allActions = store.list({ type: "action" });
|
|
15309
|
+
const openActions = allActions.filter((d) => d.frontmatter.status === "open");
|
|
15310
|
+
const doneActions = allActions.filter((d) => d.frontmatter.status === "done");
|
|
15311
|
+
const allDocs = store.list();
|
|
15312
|
+
const blockedItems = allDocs.filter(
|
|
15313
|
+
(d) => d.frontmatter.tags?.includes("blocked")
|
|
15314
|
+
);
|
|
15315
|
+
const overdueItems = allDocs.filter(
|
|
15316
|
+
(d) => d.frontmatter.tags?.includes("overdue")
|
|
15317
|
+
);
|
|
15318
|
+
const openQuestions = store.list({ type: "question", status: "open" });
|
|
15319
|
+
const riskItems = allDocs.filter(
|
|
15320
|
+
(d) => d.frontmatter.tags?.includes("risk")
|
|
15321
|
+
);
|
|
15322
|
+
const unownedActions = openActions.filter((d) => !d.frontmatter.owner);
|
|
15323
|
+
const total = allActions.length;
|
|
15324
|
+
const done = doneActions.length;
|
|
15325
|
+
const completionPct = total > 0 ? Math.round(done / total * 100) : 100;
|
|
15326
|
+
const scheduleItems = [
|
|
15327
|
+
...blockedItems,
|
|
15328
|
+
...overdueItems
|
|
15329
|
+
].filter(
|
|
15330
|
+
(d, i, arr) => arr.findIndex((x) => x.frontmatter.id === d.frontmatter.id) === i
|
|
15331
|
+
).map((d) => ({ id: d.frontmatter.id, title: d.frontmatter.title }));
|
|
15332
|
+
const qualityItems = [
|
|
15333
|
+
...riskItems,
|
|
15334
|
+
...openQuestions
|
|
15335
|
+
].filter(
|
|
15336
|
+
(d, i, arr) => arr.findIndex((x) => x.frontmatter.id === d.frontmatter.id) === i
|
|
15337
|
+
).map((d) => ({ id: d.frontmatter.id, title: d.frontmatter.title }));
|
|
15338
|
+
const resourceItems = unownedActions.map((d) => ({
|
|
15339
|
+
id: d.frontmatter.id,
|
|
15340
|
+
title: d.frontmatter.title
|
|
15341
|
+
}));
|
|
15342
|
+
return {
|
|
15343
|
+
scope: {
|
|
15344
|
+
total,
|
|
15345
|
+
open: openActions.length,
|
|
15346
|
+
done,
|
|
15347
|
+
completionPct
|
|
15348
|
+
},
|
|
15349
|
+
schedule: {
|
|
15350
|
+
blocked: blockedItems.length,
|
|
15351
|
+
overdue: overdueItems.length,
|
|
15352
|
+
items: scheduleItems
|
|
15353
|
+
},
|
|
15354
|
+
quality: {
|
|
15355
|
+
risks: riskItems.length,
|
|
15356
|
+
openQuestions: openQuestions.length,
|
|
15357
|
+
items: qualityItems
|
|
15358
|
+
},
|
|
15359
|
+
resources: {
|
|
15360
|
+
unowned: unownedActions.length,
|
|
15361
|
+
items: resourceItems
|
|
15362
|
+
}
|
|
15363
|
+
};
|
|
15364
|
+
}
|
|
15365
|
+
|
|
15366
|
+
// src/reports/gar/evaluator.ts
|
|
15367
|
+
function worstStatus(statuses) {
|
|
15368
|
+
if (statuses.includes("red")) return "red";
|
|
15369
|
+
if (statuses.includes("amber")) return "amber";
|
|
15370
|
+
return "green";
|
|
15371
|
+
}
|
|
15372
|
+
function evaluateGar(projectName, metrics) {
|
|
15373
|
+
const areas = [];
|
|
15374
|
+
const scopePct = metrics.scope.completionPct;
|
|
15375
|
+
const scopeStatus = scopePct >= 70 ? "green" : scopePct >= 40 ? "amber" : "red";
|
|
15376
|
+
areas.push({
|
|
15377
|
+
name: "Scope",
|
|
15378
|
+
status: scopeStatus,
|
|
15379
|
+
summary: `${scopePct}% complete (${metrics.scope.done}/${metrics.scope.total})`,
|
|
15380
|
+
items: []
|
|
15381
|
+
});
|
|
15382
|
+
const scheduleCount = metrics.schedule.blocked + metrics.schedule.overdue;
|
|
15383
|
+
const scheduleStatus = scheduleCount === 0 ? "green" : scheduleCount <= 2 ? "amber" : "red";
|
|
15384
|
+
const scheduleParts = [];
|
|
15385
|
+
if (metrics.schedule.blocked > 0)
|
|
15386
|
+
scheduleParts.push(`${metrics.schedule.blocked} blocked`);
|
|
15387
|
+
if (metrics.schedule.overdue > 0)
|
|
15388
|
+
scheduleParts.push(`${metrics.schedule.overdue} overdue`);
|
|
15389
|
+
areas.push({
|
|
15390
|
+
name: "Schedule",
|
|
15391
|
+
status: scheduleStatus,
|
|
15392
|
+
summary: scheduleParts.length > 0 ? scheduleParts.join(", ") : "on track",
|
|
15393
|
+
items: metrics.schedule.items
|
|
15394
|
+
});
|
|
15395
|
+
const qualityCount = metrics.quality.risks + metrics.quality.openQuestions;
|
|
15396
|
+
const qualityStatus = qualityCount === 0 ? "green" : qualityCount <= 2 ? "amber" : "red";
|
|
15397
|
+
const qualityParts = [];
|
|
15398
|
+
if (metrics.quality.risks > 0)
|
|
15399
|
+
qualityParts.push(`${metrics.quality.risks} risk(s)`);
|
|
15400
|
+
if (metrics.quality.openQuestions > 0)
|
|
15401
|
+
qualityParts.push(`${metrics.quality.openQuestions} open question(s)`);
|
|
15402
|
+
areas.push({
|
|
15403
|
+
name: "Quality",
|
|
15404
|
+
status: qualityStatus,
|
|
15405
|
+
summary: qualityParts.length > 0 ? qualityParts.join(", ") : "no issues",
|
|
15406
|
+
items: metrics.quality.items
|
|
15407
|
+
});
|
|
15408
|
+
const resourceCount = metrics.resources.unowned;
|
|
15409
|
+
const resourceStatus = resourceCount === 0 ? "green" : resourceCount <= 2 ? "amber" : "red";
|
|
15410
|
+
areas.push({
|
|
15411
|
+
name: "Resources",
|
|
15412
|
+
status: resourceStatus,
|
|
15413
|
+
summary: resourceCount > 0 ? `${resourceCount} unowned action(s)` : "all assigned",
|
|
15414
|
+
items: metrics.resources.items
|
|
15415
|
+
});
|
|
15416
|
+
const overall = worstStatus(areas.map((a) => a.status));
|
|
15417
|
+
return {
|
|
15418
|
+
projectName,
|
|
15419
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
15420
|
+
overall,
|
|
15421
|
+
areas,
|
|
15422
|
+
metrics
|
|
15423
|
+
};
|
|
15424
|
+
}
|
|
15425
|
+
|
|
15426
|
+
// src/plugins/builtin/tools/reports.ts
|
|
15305
15427
|
function createReportTools(store) {
|
|
15306
15428
|
return [
|
|
15307
15429
|
tool8(
|
|
@@ -15390,41 +15512,10 @@ function createReportTools(store) {
|
|
|
15390
15512
|
"Generate a Green-Amber-Red report with metrics across scope, schedule, quality, and resources",
|
|
15391
15513
|
{},
|
|
15392
15514
|
async () => {
|
|
15393
|
-
const
|
|
15394
|
-
const
|
|
15395
|
-
const doneActions = allActions.filter((d) => d.frontmatter.status === "done");
|
|
15396
|
-
const allDocs = store.list();
|
|
15397
|
-
const blockedItems = allDocs.filter(
|
|
15398
|
-
(d) => d.frontmatter.tags?.includes("blocked")
|
|
15399
|
-
);
|
|
15400
|
-
const overdueItems = allDocs.filter(
|
|
15401
|
-
(d) => d.frontmatter.tags?.includes("overdue")
|
|
15402
|
-
);
|
|
15403
|
-
const openQuestions = store.list({ type: "question", status: "open" });
|
|
15404
|
-
const riskItems = allDocs.filter(
|
|
15405
|
-
(d) => d.frontmatter.tags?.includes("risk")
|
|
15406
|
-
);
|
|
15407
|
-
const unownedActions = openActions.filter((d) => !d.frontmatter.owner);
|
|
15408
|
-
const areas = {
|
|
15409
|
-
scope: {
|
|
15410
|
-
total: allActions.length,
|
|
15411
|
-
open: openActions.length,
|
|
15412
|
-
done: doneActions.length
|
|
15413
|
-
},
|
|
15414
|
-
schedule: {
|
|
15415
|
-
blocked: blockedItems.length,
|
|
15416
|
-
overdue: overdueItems.length
|
|
15417
|
-
},
|
|
15418
|
-
quality: {
|
|
15419
|
-
openQuestions: openQuestions.length,
|
|
15420
|
-
risks: riskItems.length
|
|
15421
|
-
},
|
|
15422
|
-
resources: {
|
|
15423
|
-
unowned: unownedActions.length
|
|
15424
|
-
}
|
|
15425
|
-
};
|
|
15515
|
+
const metrics = collectGarMetrics(store);
|
|
15516
|
+
const report = evaluateGar("project", metrics);
|
|
15426
15517
|
return {
|
|
15427
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
15518
|
+
content: [{ type: "text", text: JSON.stringify(report, null, 2) }]
|
|
15428
15519
|
};
|
|
15429
15520
|
},
|
|
15430
15521
|
{ annotations: { readOnly: true } }
|
|
@@ -16204,6 +16295,188 @@ function createSprintTools(store) {
|
|
|
16204
16295
|
];
|
|
16205
16296
|
}
|
|
16206
16297
|
|
|
16298
|
+
// src/plugins/builtin/tools/sprint-planning.ts
|
|
16299
|
+
import { tool as tool13 } from "@anthropic-ai/claude-agent-sdk";
|
|
16300
|
+
var PRIORITY_ORDER = {
|
|
16301
|
+
critical: 0,
|
|
16302
|
+
high: 1,
|
|
16303
|
+
medium: 2,
|
|
16304
|
+
low: 3
|
|
16305
|
+
};
|
|
16306
|
+
function priorityRank(p) {
|
|
16307
|
+
return PRIORITY_ORDER[p ?? ""] ?? 99;
|
|
16308
|
+
}
|
|
16309
|
+
function createSprintPlanningTools(store) {
|
|
16310
|
+
return [
|
|
16311
|
+
tool13(
|
|
16312
|
+
"gather_sprint_planning_context",
|
|
16313
|
+
"Aggregate all planning-relevant data for proposing the next sprint: approved features, backlog epics, active sprint, velocity reference, blockers, and summary stats",
|
|
16314
|
+
{
|
|
16315
|
+
focusFeature: external_exports.string().optional().describe("Filter backlog to epics of a specific feature ID (e.g. 'F-001')"),
|
|
16316
|
+
sprintDurationDays: external_exports.number().optional().describe("Expected sprint duration in days \u2014 passed through for capacity reasoning")
|
|
16317
|
+
},
|
|
16318
|
+
async (args) => {
|
|
16319
|
+
const features = store.list({ type: "feature" });
|
|
16320
|
+
const epics = store.list({ type: "epic" });
|
|
16321
|
+
const sprints = store.list({ type: "sprint" });
|
|
16322
|
+
const questions = store.list({ type: "question", status: "open" });
|
|
16323
|
+
const contributions = store.list({ type: "contribution" });
|
|
16324
|
+
const approvedFeatures = features.filter((f) => f.frontmatter.status === "approved").sort((a, b) => priorityRank(a.frontmatter.priority) - priorityRank(b.frontmatter.priority)).map((f) => {
|
|
16325
|
+
const linkedEpics = epics.filter((e) => e.frontmatter.linkedFeature === f.frontmatter.id);
|
|
16326
|
+
const epicsByStatus = {};
|
|
16327
|
+
for (const e of linkedEpics) {
|
|
16328
|
+
epicsByStatus[e.frontmatter.status] = (epicsByStatus[e.frontmatter.status] ?? 0) + 1;
|
|
16329
|
+
}
|
|
16330
|
+
return {
|
|
16331
|
+
id: f.frontmatter.id,
|
|
16332
|
+
title: f.frontmatter.title,
|
|
16333
|
+
priority: f.frontmatter.priority,
|
|
16334
|
+
owner: f.frontmatter.owner,
|
|
16335
|
+
epicCount: linkedEpics.length,
|
|
16336
|
+
epicsByStatus
|
|
16337
|
+
};
|
|
16338
|
+
});
|
|
16339
|
+
const assignedEpicIds = /* @__PURE__ */ new Set();
|
|
16340
|
+
for (const sp of sprints) {
|
|
16341
|
+
const linked = sp.frontmatter.linkedEpics ?? [];
|
|
16342
|
+
for (const id of linked) assignedEpicIds.add(id);
|
|
16343
|
+
}
|
|
16344
|
+
const featureMap = new Map(features.map((f) => [f.frontmatter.id, f]));
|
|
16345
|
+
let backlogEpics = epics.filter(
|
|
16346
|
+
(e) => !assignedEpicIds.has(e.frontmatter.id) && e.frontmatter.status !== "done"
|
|
16347
|
+
);
|
|
16348
|
+
if (args.focusFeature) {
|
|
16349
|
+
backlogEpics = backlogEpics.filter(
|
|
16350
|
+
(e) => e.frontmatter.linkedFeature === args.focusFeature
|
|
16351
|
+
);
|
|
16352
|
+
}
|
|
16353
|
+
const backlog = backlogEpics.sort((a, b) => {
|
|
16354
|
+
const fa = featureMap.get(a.frontmatter.linkedFeature);
|
|
16355
|
+
const fb = featureMap.get(b.frontmatter.linkedFeature);
|
|
16356
|
+
return priorityRank(fa?.frontmatter.priority) - priorityRank(fb?.frontmatter.priority);
|
|
16357
|
+
}).map((e) => {
|
|
16358
|
+
const parent = featureMap.get(e.frontmatter.linkedFeature);
|
|
16359
|
+
return {
|
|
16360
|
+
id: e.frontmatter.id,
|
|
16361
|
+
title: e.frontmatter.title,
|
|
16362
|
+
status: e.frontmatter.status,
|
|
16363
|
+
linkedFeature: e.frontmatter.linkedFeature,
|
|
16364
|
+
featureTitle: parent?.frontmatter.title ?? null,
|
|
16365
|
+
featurePriority: parent?.frontmatter.priority ?? null,
|
|
16366
|
+
estimatedEffort: e.frontmatter.estimatedEffort ?? null,
|
|
16367
|
+
targetDate: e.frontmatter.targetDate ?? null
|
|
16368
|
+
};
|
|
16369
|
+
});
|
|
16370
|
+
const activeSprintDoc = sprints.find((s) => s.frontmatter.status === "active") ?? null;
|
|
16371
|
+
let activeSprint = null;
|
|
16372
|
+
if (activeSprintDoc) {
|
|
16373
|
+
const linkedEpicIds = activeSprintDoc.frontmatter.linkedEpics ?? [];
|
|
16374
|
+
const linkedEpics = linkedEpicIds.map((epicId) => {
|
|
16375
|
+
const epic = store.get(epicId);
|
|
16376
|
+
return epic ? { id: epicId, title: epic.frontmatter.title, status: epic.frontmatter.status } : { id: epicId, title: "(not found)", status: "unknown" };
|
|
16377
|
+
});
|
|
16378
|
+
const allDocs = store.list();
|
|
16379
|
+
const sprintTag = `sprint:${activeSprintDoc.frontmatter.id}`;
|
|
16380
|
+
const workItems = allDocs.filter(
|
|
16381
|
+
(d) => d.frontmatter.type !== "sprint" && d.frontmatter.type !== "epic" && d.frontmatter.tags?.includes(sprintTag)
|
|
16382
|
+
);
|
|
16383
|
+
const doneCount = workItems.filter(
|
|
16384
|
+
(d) => d.frontmatter.status === "done" || d.frontmatter.status === "resolved" || d.frontmatter.status === "closed"
|
|
16385
|
+
).length;
|
|
16386
|
+
const completionPct = workItems.length > 0 ? Math.round(doneCount / workItems.length * 100) : 0;
|
|
16387
|
+
activeSprint = {
|
|
16388
|
+
id: activeSprintDoc.frontmatter.id,
|
|
16389
|
+
title: activeSprintDoc.frontmatter.title,
|
|
16390
|
+
goal: activeSprintDoc.frontmatter.goal,
|
|
16391
|
+
startDate: activeSprintDoc.frontmatter.startDate,
|
|
16392
|
+
endDate: activeSprintDoc.frontmatter.endDate,
|
|
16393
|
+
linkedEpics,
|
|
16394
|
+
workItems: { total: workItems.length, done: doneCount, completionPct }
|
|
16395
|
+
};
|
|
16396
|
+
}
|
|
16397
|
+
const completedSprints = sprints.filter((s) => s.frontmatter.status === "completed").sort((a, b) => {
|
|
16398
|
+
const da = a.frontmatter.endDate ?? "";
|
|
16399
|
+
const db = b.frontmatter.endDate ?? "";
|
|
16400
|
+
return da < db ? 1 : da > db ? -1 : 0;
|
|
16401
|
+
}).slice(0, 2);
|
|
16402
|
+
const velocityReference = completedSprints.map((sp) => {
|
|
16403
|
+
const linkedEpicIds = sp.frontmatter.linkedEpics ?? [];
|
|
16404
|
+
const efforts = [];
|
|
16405
|
+
for (const epicId of linkedEpicIds) {
|
|
16406
|
+
const epic = store.get(epicId);
|
|
16407
|
+
if (epic?.frontmatter.estimatedEffort) {
|
|
16408
|
+
efforts.push(String(epic.frontmatter.estimatedEffort));
|
|
16409
|
+
}
|
|
16410
|
+
}
|
|
16411
|
+
const allDocs = store.list();
|
|
16412
|
+
const sprintTag = `sprint:${sp.frontmatter.id}`;
|
|
16413
|
+
const workItems = allDocs.filter(
|
|
16414
|
+
(d) => d.frontmatter.type !== "sprint" && d.frontmatter.type !== "epic" && d.frontmatter.tags?.includes(sprintTag)
|
|
16415
|
+
);
|
|
16416
|
+
return {
|
|
16417
|
+
id: sp.frontmatter.id,
|
|
16418
|
+
title: sp.frontmatter.title,
|
|
16419
|
+
startDate: sp.frontmatter.startDate,
|
|
16420
|
+
endDate: sp.frontmatter.endDate,
|
|
16421
|
+
epicCount: linkedEpicIds.length,
|
|
16422
|
+
efforts,
|
|
16423
|
+
workItemCount: workItems.length
|
|
16424
|
+
};
|
|
16425
|
+
});
|
|
16426
|
+
const openBlockerContributions = contributions.filter(
|
|
16427
|
+
(c) => c.frontmatter.status === "open" && (c.frontmatter.contributionType === "risk-finding" || c.frontmatter.contributionType === "blocker-report")
|
|
16428
|
+
);
|
|
16429
|
+
const blockers = {
|
|
16430
|
+
openQuestions: questions.map((q) => ({
|
|
16431
|
+
id: q.frontmatter.id,
|
|
16432
|
+
title: q.frontmatter.title
|
|
16433
|
+
})),
|
|
16434
|
+
openRiskAndBlockerContributions: openBlockerContributions.map((c) => ({
|
|
16435
|
+
id: c.frontmatter.id,
|
|
16436
|
+
title: c.frontmatter.title,
|
|
16437
|
+
contributionType: c.frontmatter.contributionType
|
|
16438
|
+
}))
|
|
16439
|
+
};
|
|
16440
|
+
const totalBacklogEfforts = backlog.filter((e) => e.estimatedEffort !== null).map((e) => String(e.estimatedEffort));
|
|
16441
|
+
const approvedFeaturesWithNoEpics = approvedFeatures.filter((f) => f.epicCount === 0).map((f) => ({ id: f.id, title: f.title }));
|
|
16442
|
+
const now = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
16443
|
+
const epicsAtRisk = epics.filter((e) => {
|
|
16444
|
+
if (e.frontmatter.status === "done") return false;
|
|
16445
|
+
if (e.frontmatter.targetDate && e.frontmatter.targetDate < now) return true;
|
|
16446
|
+
const parent = featureMap.get(e.frontmatter.linkedFeature);
|
|
16447
|
+
if (parent?.frontmatter.status === "deferred") return true;
|
|
16448
|
+
return false;
|
|
16449
|
+
}).map((e) => ({
|
|
16450
|
+
id: e.frontmatter.id,
|
|
16451
|
+
title: e.frontmatter.title,
|
|
16452
|
+
reason: e.frontmatter.targetDate && e.frontmatter.targetDate < now ? "past-target-date" : "deferred-feature"
|
|
16453
|
+
}));
|
|
16454
|
+
const plannedSprintCount = sprints.filter((s) => s.frontmatter.status === "planned").length;
|
|
16455
|
+
const summary = {
|
|
16456
|
+
totalBacklogEpics: backlog.length,
|
|
16457
|
+
totalBacklogEfforts,
|
|
16458
|
+
approvedFeaturesWithNoEpics,
|
|
16459
|
+
epicsAtRisk,
|
|
16460
|
+
plannedSprintCount
|
|
16461
|
+
};
|
|
16462
|
+
const result = {
|
|
16463
|
+
approvedFeatures,
|
|
16464
|
+
backlog,
|
|
16465
|
+
activeSprint,
|
|
16466
|
+
velocityReference,
|
|
16467
|
+
blockers,
|
|
16468
|
+
summary,
|
|
16469
|
+
...args.sprintDurationDays !== void 0 ? { sprintDurationDays: args.sprintDurationDays } : {}
|
|
16470
|
+
};
|
|
16471
|
+
return {
|
|
16472
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
16473
|
+
};
|
|
16474
|
+
},
|
|
16475
|
+
{ annotations: { readOnly: true } }
|
|
16476
|
+
)
|
|
16477
|
+
];
|
|
16478
|
+
}
|
|
16479
|
+
|
|
16207
16480
|
// src/plugins/common.ts
|
|
16208
16481
|
var COMMON_REGISTRATIONS = [
|
|
16209
16482
|
{ type: "meeting", dirName: "meetings", idPrefix: "M" },
|
|
@@ -16220,7 +16493,8 @@ function createCommonTools(store) {
|
|
|
16220
16493
|
...createFeatureTools(store),
|
|
16221
16494
|
...createEpicTools(store),
|
|
16222
16495
|
...createContributionTools(store),
|
|
16223
|
-
...createSprintTools(store)
|
|
16496
|
+
...createSprintTools(store),
|
|
16497
|
+
...createSprintPlanningTools(store)
|
|
16224
16498
|
];
|
|
16225
16499
|
}
|
|
16226
16500
|
|
|
@@ -16293,7 +16567,12 @@ var genericAgilePlugin = {
|
|
|
16293
16567
|
- **list_sprints** / **get_sprint**: View sprints to understand iteration scope and delivery dates.
|
|
16294
16568
|
- **update_sprint**: Assign epics to sprints by updating linkedEpics when breaking features into work.
|
|
16295
16569
|
- Tag technical actions and decisions with \`sprint:SP-xxx\` to associate them with a sprint.
|
|
16296
|
-
- Use **generate_sprint_progress** to track technical work completion within an iteration
|
|
16570
|
+
- Use **generate_sprint_progress** to track technical work completion within an iteration.
|
|
16571
|
+
|
|
16572
|
+
**Sprint Planning:**
|
|
16573
|
+
- When asked to plan or propose a sprint, ALWAYS call **gather_sprint_planning_context** first.
|
|
16574
|
+
- Focus on: technical readiness of each epic, open technical questions or spikes, effort balance across the sprint, and feature coverage.
|
|
16575
|
+
- Present a structured sprint proposal with technical rationale for each selected epic, known technical risks, and any prerequisite work that should be completed first.`,
|
|
16297
16576
|
"delivery-manager": `You track delivery across features and epics, manage schedules, and report on progress.
|
|
16298
16577
|
|
|
16299
16578
|
**Report Tools:**
|
|
@@ -16341,7 +16620,13 @@ var genericAgilePlugin = {
|
|
|
16341
16620
|
- Assign epics to sprints via linkedEpics.
|
|
16342
16621
|
- Tag work items (actions, decisions, questions) with \`sprint:SP-xxx\` for sprint scoping.
|
|
16343
16622
|
- Track delivery dates and flag at-risk sprints.
|
|
16344
|
-
- Register past/completed sprints for historical tracking
|
|
16623
|
+
- Register past/completed sprints for historical tracking.
|
|
16624
|
+
|
|
16625
|
+
**Sprint Planning:**
|
|
16626
|
+
- 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.
|
|
16627
|
+
- Reason through: priority (critical/high features first), capacity (compare backlog effort to velocity reference), dependencies and blockers, balance across features, and risk.
|
|
16628
|
+
- Present a structured sprint proposal: title, goal, suggested dates, selected epics with rationale for each, excluded epics with reason, and identified risks.
|
|
16629
|
+
- After user confirmation, use **create_sprint** with the agreed epics to persist the sprint.`,
|
|
16345
16630
|
"*": `You have access to feature, epic, sprint, and meeting tools for project coordination:
|
|
16346
16631
|
|
|
16347
16632
|
**Features** (F-xxx): Product capabilities defined by the Product Owner. Features progress through draft \u2192 approved \u2192 done.
|
|
@@ -16364,10 +16649,10 @@ var genericAgilePlugin = {
|
|
|
16364
16649
|
};
|
|
16365
16650
|
|
|
16366
16651
|
// src/plugins/builtin/tools/use-cases.ts
|
|
16367
|
-
import { tool as
|
|
16652
|
+
import { tool as tool14 } from "@anthropic-ai/claude-agent-sdk";
|
|
16368
16653
|
function createUseCaseTools(store) {
|
|
16369
16654
|
return [
|
|
16370
|
-
|
|
16655
|
+
tool14(
|
|
16371
16656
|
"list_use_cases",
|
|
16372
16657
|
"List all extension use cases, optionally filtered by status or extension type",
|
|
16373
16658
|
{
|
|
@@ -16397,7 +16682,7 @@ function createUseCaseTools(store) {
|
|
|
16397
16682
|
},
|
|
16398
16683
|
{ annotations: { readOnly: true } }
|
|
16399
16684
|
),
|
|
16400
|
-
|
|
16685
|
+
tool14(
|
|
16401
16686
|
"get_use_case",
|
|
16402
16687
|
"Get the full content of a specific use case by ID",
|
|
16403
16688
|
{ id: external_exports.string().describe("Use case ID (e.g. 'UC-001')") },
|
|
@@ -16424,7 +16709,7 @@ function createUseCaseTools(store) {
|
|
|
16424
16709
|
},
|
|
16425
16710
|
{ annotations: { readOnly: true } }
|
|
16426
16711
|
),
|
|
16427
|
-
|
|
16712
|
+
tool14(
|
|
16428
16713
|
"create_use_case",
|
|
16429
16714
|
"Create a new extension use case definition (Phase 1: Assess Extension Use Case)",
|
|
16430
16715
|
{
|
|
@@ -16458,7 +16743,7 @@ function createUseCaseTools(store) {
|
|
|
16458
16743
|
};
|
|
16459
16744
|
}
|
|
16460
16745
|
),
|
|
16461
|
-
|
|
16746
|
+
tool14(
|
|
16462
16747
|
"update_use_case",
|
|
16463
16748
|
"Update an existing extension use case",
|
|
16464
16749
|
{
|
|
@@ -16488,10 +16773,10 @@ function createUseCaseTools(store) {
|
|
|
16488
16773
|
}
|
|
16489
16774
|
|
|
16490
16775
|
// src/plugins/builtin/tools/tech-assessments.ts
|
|
16491
|
-
import { tool as
|
|
16776
|
+
import { tool as tool15 } from "@anthropic-ai/claude-agent-sdk";
|
|
16492
16777
|
function createTechAssessmentTools(store) {
|
|
16493
16778
|
return [
|
|
16494
|
-
|
|
16779
|
+
tool15(
|
|
16495
16780
|
"list_tech_assessments",
|
|
16496
16781
|
"List all technology assessments, optionally filtered by status",
|
|
16497
16782
|
{
|
|
@@ -16522,7 +16807,7 @@ function createTechAssessmentTools(store) {
|
|
|
16522
16807
|
},
|
|
16523
16808
|
{ annotations: { readOnly: true } }
|
|
16524
16809
|
),
|
|
16525
|
-
|
|
16810
|
+
tool15(
|
|
16526
16811
|
"get_tech_assessment",
|
|
16527
16812
|
"Get the full content of a specific technology assessment by ID",
|
|
16528
16813
|
{ id: external_exports.string().describe("Tech assessment ID (e.g. 'TA-001')") },
|
|
@@ -16549,7 +16834,7 @@ function createTechAssessmentTools(store) {
|
|
|
16549
16834
|
},
|
|
16550
16835
|
{ annotations: { readOnly: true } }
|
|
16551
16836
|
),
|
|
16552
|
-
|
|
16837
|
+
tool15(
|
|
16553
16838
|
"create_tech_assessment",
|
|
16554
16839
|
"Create a new technology assessment linked to an assessed/approved use case (Phase 2: Assess Extension Technology)",
|
|
16555
16840
|
{
|
|
@@ -16620,7 +16905,7 @@ function createTechAssessmentTools(store) {
|
|
|
16620
16905
|
};
|
|
16621
16906
|
}
|
|
16622
16907
|
),
|
|
16623
|
-
|
|
16908
|
+
tool15(
|
|
16624
16909
|
"update_tech_assessment",
|
|
16625
16910
|
"Update an existing technology assessment. The linked use case cannot be changed.",
|
|
16626
16911
|
{
|
|
@@ -16650,10 +16935,10 @@ function createTechAssessmentTools(store) {
|
|
|
16650
16935
|
}
|
|
16651
16936
|
|
|
16652
16937
|
// src/plugins/builtin/tools/extension-designs.ts
|
|
16653
|
-
import { tool as
|
|
16938
|
+
import { tool as tool16 } from "@anthropic-ai/claude-agent-sdk";
|
|
16654
16939
|
function createExtensionDesignTools(store) {
|
|
16655
16940
|
return [
|
|
16656
|
-
|
|
16941
|
+
tool16(
|
|
16657
16942
|
"list_extension_designs",
|
|
16658
16943
|
"List all extension designs, optionally filtered by status",
|
|
16659
16944
|
{
|
|
@@ -16683,7 +16968,7 @@ function createExtensionDesignTools(store) {
|
|
|
16683
16968
|
},
|
|
16684
16969
|
{ annotations: { readOnly: true } }
|
|
16685
16970
|
),
|
|
16686
|
-
|
|
16971
|
+
tool16(
|
|
16687
16972
|
"get_extension_design",
|
|
16688
16973
|
"Get the full content of a specific extension design by ID",
|
|
16689
16974
|
{ id: external_exports.string().describe("Extension design ID (e.g. 'XD-001')") },
|
|
@@ -16710,7 +16995,7 @@ function createExtensionDesignTools(store) {
|
|
|
16710
16995
|
},
|
|
16711
16996
|
{ annotations: { readOnly: true } }
|
|
16712
16997
|
),
|
|
16713
|
-
|
|
16998
|
+
tool16(
|
|
16714
16999
|
"create_extension_design",
|
|
16715
17000
|
"Create a new extension design linked to a recommended tech assessment (Phase 3: Define Extension Target Solution)",
|
|
16716
17001
|
{
|
|
@@ -16778,7 +17063,7 @@ function createExtensionDesignTools(store) {
|
|
|
16778
17063
|
};
|
|
16779
17064
|
}
|
|
16780
17065
|
),
|
|
16781
|
-
|
|
17066
|
+
tool16(
|
|
16782
17067
|
"update_extension_design",
|
|
16783
17068
|
"Update an existing extension design. The linked tech assessment cannot be changed.",
|
|
16784
17069
|
{
|
|
@@ -16807,10 +17092,10 @@ function createExtensionDesignTools(store) {
|
|
|
16807
17092
|
}
|
|
16808
17093
|
|
|
16809
17094
|
// src/plugins/builtin/tools/aem-reports.ts
|
|
16810
|
-
import { tool as
|
|
17095
|
+
import { tool as tool17 } from "@anthropic-ai/claude-agent-sdk";
|
|
16811
17096
|
function createAemReportTools(store) {
|
|
16812
17097
|
return [
|
|
16813
|
-
|
|
17098
|
+
tool17(
|
|
16814
17099
|
"generate_extension_portfolio",
|
|
16815
17100
|
"Generate a portfolio view of all use cases with their linked tech assessments and extension designs",
|
|
16816
17101
|
{},
|
|
@@ -16862,7 +17147,7 @@ function createAemReportTools(store) {
|
|
|
16862
17147
|
},
|
|
16863
17148
|
{ annotations: { readOnly: true } }
|
|
16864
17149
|
),
|
|
16865
|
-
|
|
17150
|
+
tool17(
|
|
16866
17151
|
"generate_tech_readiness",
|
|
16867
17152
|
"Generate a BTP technology readiness report showing service coverage and gaps across assessments",
|
|
16868
17153
|
{},
|
|
@@ -16914,7 +17199,7 @@ function createAemReportTools(store) {
|
|
|
16914
17199
|
},
|
|
16915
17200
|
{ annotations: { readOnly: true } }
|
|
16916
17201
|
),
|
|
16917
|
-
|
|
17202
|
+
tool17(
|
|
16918
17203
|
"generate_phase_status",
|
|
16919
17204
|
"Generate a phase progress report showing artifact counts and readiness per AEM phase",
|
|
16920
17205
|
{},
|
|
@@ -16976,11 +17261,11 @@ function createAemReportTools(store) {
|
|
|
16976
17261
|
import * as fs6 from "fs";
|
|
16977
17262
|
import * as path6 from "path";
|
|
16978
17263
|
import * as YAML4 from "yaml";
|
|
16979
|
-
import { tool as
|
|
17264
|
+
import { tool as tool18 } from "@anthropic-ai/claude-agent-sdk";
|
|
16980
17265
|
var PHASES = ["assess-use-case", "assess-technology", "define-solution"];
|
|
16981
17266
|
function createAemPhaseTools(store, marvinDir) {
|
|
16982
17267
|
return [
|
|
16983
|
-
|
|
17268
|
+
tool18(
|
|
16984
17269
|
"get_current_phase",
|
|
16985
17270
|
"Get the current AEM phase from project configuration",
|
|
16986
17271
|
{},
|
|
@@ -17001,7 +17286,7 @@ function createAemPhaseTools(store, marvinDir) {
|
|
|
17001
17286
|
},
|
|
17002
17287
|
{ annotations: { readOnly: true } }
|
|
17003
17288
|
),
|
|
17004
|
-
|
|
17289
|
+
tool18(
|
|
17005
17290
|
"advance_phase",
|
|
17006
17291
|
"Advance to the next AEM phase. Performs soft gate checks and warns if artifacts are incomplete, but does not block.",
|
|
17007
17292
|
{
|
|
@@ -17308,9 +17593,495 @@ Be thorough but concise. Focus on actionable insights.`,
|
|
|
17308
17593
|
]
|
|
17309
17594
|
};
|
|
17310
17595
|
|
|
17596
|
+
// src/skills/builtin/jira/tools.ts
|
|
17597
|
+
import { tool as tool19 } from "@anthropic-ai/claude-agent-sdk";
|
|
17598
|
+
|
|
17599
|
+
// src/skills/builtin/jira/client.ts
|
|
17600
|
+
var JiraClient = class {
|
|
17601
|
+
baseUrl;
|
|
17602
|
+
authHeader;
|
|
17603
|
+
constructor(config2) {
|
|
17604
|
+
this.baseUrl = `https://${config2.host}/rest/api/2`;
|
|
17605
|
+
this.authHeader = "Basic " + Buffer.from(`${config2.email}:${config2.apiToken}`).toString("base64");
|
|
17606
|
+
}
|
|
17607
|
+
async request(path18, method = "GET", body) {
|
|
17608
|
+
const url2 = `${this.baseUrl}${path18}`;
|
|
17609
|
+
const headers = {
|
|
17610
|
+
Authorization: this.authHeader,
|
|
17611
|
+
"Content-Type": "application/json",
|
|
17612
|
+
Accept: "application/json"
|
|
17613
|
+
};
|
|
17614
|
+
const response = await fetch(url2, {
|
|
17615
|
+
method,
|
|
17616
|
+
headers,
|
|
17617
|
+
body: body ? JSON.stringify(body) : void 0
|
|
17618
|
+
});
|
|
17619
|
+
if (!response.ok) {
|
|
17620
|
+
const text = await response.text().catch(() => "");
|
|
17621
|
+
throw new Error(
|
|
17622
|
+
`Jira API error ${response.status} ${method} ${path18}: ${text}`
|
|
17623
|
+
);
|
|
17624
|
+
}
|
|
17625
|
+
if (response.status === 204) return void 0;
|
|
17626
|
+
return response.json();
|
|
17627
|
+
}
|
|
17628
|
+
async searchIssues(jql, maxResults = 50) {
|
|
17629
|
+
const params = new URLSearchParams({
|
|
17630
|
+
jql,
|
|
17631
|
+
maxResults: String(maxResults)
|
|
17632
|
+
});
|
|
17633
|
+
return this.request(`/search?${params}`);
|
|
17634
|
+
}
|
|
17635
|
+
async getIssue(key) {
|
|
17636
|
+
return this.request(`/issue/${encodeURIComponent(key)}`);
|
|
17637
|
+
}
|
|
17638
|
+
async createIssue(fields) {
|
|
17639
|
+
return this.request("/issue", "POST", { fields });
|
|
17640
|
+
}
|
|
17641
|
+
async updateIssue(key, fields) {
|
|
17642
|
+
await this.request(
|
|
17643
|
+
`/issue/${encodeURIComponent(key)}`,
|
|
17644
|
+
"PUT",
|
|
17645
|
+
{ fields }
|
|
17646
|
+
);
|
|
17647
|
+
}
|
|
17648
|
+
async addComment(key, body) {
|
|
17649
|
+
await this.request(
|
|
17650
|
+
`/issue/${encodeURIComponent(key)}/comment`,
|
|
17651
|
+
"POST",
|
|
17652
|
+
{ body }
|
|
17653
|
+
);
|
|
17654
|
+
}
|
|
17655
|
+
};
|
|
17656
|
+
function createJiraClient() {
|
|
17657
|
+
const host = process.env.JIRA_HOST;
|
|
17658
|
+
const email3 = process.env.JIRA_EMAIL;
|
|
17659
|
+
const apiToken = process.env.JIRA_API_TOKEN;
|
|
17660
|
+
if (!host || !email3 || !apiToken) return null;
|
|
17661
|
+
return new JiraClient({ host, email: email3, apiToken });
|
|
17662
|
+
}
|
|
17663
|
+
|
|
17664
|
+
// src/skills/builtin/jira/tools.ts
|
|
17665
|
+
var JIRA_TYPE = "jira-issue";
|
|
17666
|
+
function jiraNotConfiguredError() {
|
|
17667
|
+
return {
|
|
17668
|
+
content: [
|
|
17669
|
+
{
|
|
17670
|
+
type: "text",
|
|
17671
|
+
text: "Jira is not configured. Set JIRA_HOST, JIRA_EMAIL, and JIRA_API_TOKEN environment variables."
|
|
17672
|
+
}
|
|
17673
|
+
],
|
|
17674
|
+
isError: true
|
|
17675
|
+
};
|
|
17676
|
+
}
|
|
17677
|
+
function mapJiraStatus(jiraStatus) {
|
|
17678
|
+
const lower = jiraStatus.toLowerCase();
|
|
17679
|
+
if (lower === "done" || lower === "closed" || lower === "resolved") return "done";
|
|
17680
|
+
if (lower === "in progress" || lower === "in review") return "in-progress";
|
|
17681
|
+
return "open";
|
|
17682
|
+
}
|
|
17683
|
+
function jiraIssueToFrontmatter(issue2, host, linkedArtifacts) {
|
|
17684
|
+
return {
|
|
17685
|
+
title: issue2.fields.summary,
|
|
17686
|
+
status: mapJiraStatus(issue2.fields.status.name),
|
|
17687
|
+
jiraKey: issue2.key,
|
|
17688
|
+
jiraUrl: `https://${host}/browse/${issue2.key}`,
|
|
17689
|
+
issueType: issue2.fields.issuetype.name,
|
|
17690
|
+
priority: issue2.fields.priority?.name ?? "None",
|
|
17691
|
+
assignee: issue2.fields.assignee?.displayName ?? "",
|
|
17692
|
+
labels: issue2.fields.labels ?? [],
|
|
17693
|
+
linkedArtifacts: linkedArtifacts ?? [],
|
|
17694
|
+
tags: [`jira:${issue2.key}`],
|
|
17695
|
+
lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
17696
|
+
};
|
|
17697
|
+
}
|
|
17698
|
+
function findByJiraKey(store, jiraKey) {
|
|
17699
|
+
const docs = store.list({ type: JIRA_TYPE });
|
|
17700
|
+
return docs.find((d) => d.frontmatter.jiraKey === jiraKey);
|
|
17701
|
+
}
|
|
17702
|
+
function createJiraTools(store) {
|
|
17703
|
+
return [
|
|
17704
|
+
// --- Local read tools ---
|
|
17705
|
+
tool19(
|
|
17706
|
+
"list_jira_issues",
|
|
17707
|
+
"List locally synced Jira issues (JI-xxx documents), optionally filtered by status or Jira key",
|
|
17708
|
+
{
|
|
17709
|
+
status: external_exports.enum(["open", "in-progress", "done"]).optional().describe("Filter by local status"),
|
|
17710
|
+
jiraKey: external_exports.string().optional().describe("Filter by Jira issue key (e.g. 'PROJ-123')")
|
|
17711
|
+
},
|
|
17712
|
+
async (args) => {
|
|
17713
|
+
let docs = store.list({ type: JIRA_TYPE, status: args.status });
|
|
17714
|
+
if (args.jiraKey) {
|
|
17715
|
+
docs = docs.filter((d) => d.frontmatter.jiraKey === args.jiraKey);
|
|
17716
|
+
}
|
|
17717
|
+
const summary = docs.map((d) => ({
|
|
17718
|
+
id: d.frontmatter.id,
|
|
17719
|
+
title: d.frontmatter.title,
|
|
17720
|
+
status: d.frontmatter.status,
|
|
17721
|
+
jiraKey: d.frontmatter.jiraKey,
|
|
17722
|
+
issueType: d.frontmatter.issueType,
|
|
17723
|
+
priority: d.frontmatter.priority,
|
|
17724
|
+
assignee: d.frontmatter.assignee,
|
|
17725
|
+
linkedArtifacts: d.frontmatter.linkedArtifacts
|
|
17726
|
+
}));
|
|
17727
|
+
return {
|
|
17728
|
+
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
17729
|
+
};
|
|
17730
|
+
},
|
|
17731
|
+
{ annotations: { readOnly: true } }
|
|
17732
|
+
),
|
|
17733
|
+
tool19(
|
|
17734
|
+
"get_jira_issue",
|
|
17735
|
+
"Get the full content of a locally synced Jira issue by local ID (JI-xxx) or Jira key (PROJ-123)",
|
|
17736
|
+
{
|
|
17737
|
+
id: external_exports.string().describe("Local ID (e.g. 'JI-001') or Jira key (e.g. 'PROJ-123')")
|
|
17738
|
+
},
|
|
17739
|
+
async (args) => {
|
|
17740
|
+
let doc = store.get(args.id);
|
|
17741
|
+
if (!doc) {
|
|
17742
|
+
doc = findByJiraKey(store, args.id);
|
|
17743
|
+
}
|
|
17744
|
+
if (!doc) {
|
|
17745
|
+
return {
|
|
17746
|
+
content: [{ type: "text", text: `Jira issue ${args.id} not found locally` }],
|
|
17747
|
+
isError: true
|
|
17748
|
+
};
|
|
17749
|
+
}
|
|
17750
|
+
return {
|
|
17751
|
+
content: [
|
|
17752
|
+
{
|
|
17753
|
+
type: "text",
|
|
17754
|
+
text: JSON.stringify(
|
|
17755
|
+
{ ...doc.frontmatter, content: doc.content },
|
|
17756
|
+
null,
|
|
17757
|
+
2
|
|
17758
|
+
)
|
|
17759
|
+
}
|
|
17760
|
+
]
|
|
17761
|
+
};
|
|
17762
|
+
},
|
|
17763
|
+
{ annotations: { readOnly: true } }
|
|
17764
|
+
),
|
|
17765
|
+
// --- Jira → Local tools ---
|
|
17766
|
+
tool19(
|
|
17767
|
+
"pull_jira_issue",
|
|
17768
|
+
"Fetch a single Jira issue by key and create/update a local JI-xxx document",
|
|
17769
|
+
{
|
|
17770
|
+
key: external_exports.string().describe("Jira issue key (e.g. 'PROJ-123')")
|
|
17771
|
+
},
|
|
17772
|
+
async (args) => {
|
|
17773
|
+
const client = createJiraClient();
|
|
17774
|
+
if (!client) return jiraNotConfiguredError();
|
|
17775
|
+
const issue2 = await client.getIssue(args.key);
|
|
17776
|
+
const host = process.env.JIRA_HOST;
|
|
17777
|
+
const existing = findByJiraKey(store, args.key);
|
|
17778
|
+
if (existing) {
|
|
17779
|
+
const fm2 = jiraIssueToFrontmatter(
|
|
17780
|
+
issue2,
|
|
17781
|
+
host,
|
|
17782
|
+
existing.frontmatter.linkedArtifacts
|
|
17783
|
+
);
|
|
17784
|
+
const doc2 = store.update(
|
|
17785
|
+
existing.frontmatter.id,
|
|
17786
|
+
fm2,
|
|
17787
|
+
issue2.fields.description ?? ""
|
|
17788
|
+
);
|
|
17789
|
+
return {
|
|
17790
|
+
content: [
|
|
17791
|
+
{
|
|
17792
|
+
type: "text",
|
|
17793
|
+
text: `Updated ${doc2.frontmatter.id} from Jira ${args.key}`
|
|
17794
|
+
}
|
|
17795
|
+
]
|
|
17796
|
+
};
|
|
17797
|
+
}
|
|
17798
|
+
const fm = jiraIssueToFrontmatter(issue2, host);
|
|
17799
|
+
const doc = store.create(
|
|
17800
|
+
JIRA_TYPE,
|
|
17801
|
+
fm,
|
|
17802
|
+
issue2.fields.description ?? ""
|
|
17803
|
+
);
|
|
17804
|
+
return {
|
|
17805
|
+
content: [
|
|
17806
|
+
{
|
|
17807
|
+
type: "text",
|
|
17808
|
+
text: `Created ${doc.frontmatter.id} from Jira ${args.key}`
|
|
17809
|
+
}
|
|
17810
|
+
]
|
|
17811
|
+
};
|
|
17812
|
+
}
|
|
17813
|
+
),
|
|
17814
|
+
tool19(
|
|
17815
|
+
"pull_jira_issues_jql",
|
|
17816
|
+
"Bulk fetch Jira issues via JQL query and create/update local JI-xxx documents",
|
|
17817
|
+
{
|
|
17818
|
+
jql: external_exports.string().describe(`JQL query (e.g. 'project = PROJ AND status = "In Progress"')`),
|
|
17819
|
+
maxResults: external_exports.number().optional().describe("Max issues to fetch (default 50)")
|
|
17820
|
+
},
|
|
17821
|
+
async (args) => {
|
|
17822
|
+
const client = createJiraClient();
|
|
17823
|
+
if (!client) return jiraNotConfiguredError();
|
|
17824
|
+
const result = await client.searchIssues(args.jql, args.maxResults);
|
|
17825
|
+
const host = process.env.JIRA_HOST;
|
|
17826
|
+
const created = [];
|
|
17827
|
+
const updated = [];
|
|
17828
|
+
for (const issue2 of result.issues) {
|
|
17829
|
+
const existing = findByJiraKey(store, issue2.key);
|
|
17830
|
+
if (existing) {
|
|
17831
|
+
const fm = jiraIssueToFrontmatter(
|
|
17832
|
+
issue2,
|
|
17833
|
+
host,
|
|
17834
|
+
existing.frontmatter.linkedArtifacts
|
|
17835
|
+
);
|
|
17836
|
+
store.update(
|
|
17837
|
+
existing.frontmatter.id,
|
|
17838
|
+
fm,
|
|
17839
|
+
issue2.fields.description ?? ""
|
|
17840
|
+
);
|
|
17841
|
+
updated.push(`${existing.frontmatter.id} (${issue2.key})`);
|
|
17842
|
+
} else {
|
|
17843
|
+
const fm = jiraIssueToFrontmatter(issue2, host);
|
|
17844
|
+
const doc = store.create(
|
|
17845
|
+
JIRA_TYPE,
|
|
17846
|
+
fm,
|
|
17847
|
+
issue2.fields.description ?? ""
|
|
17848
|
+
);
|
|
17849
|
+
created.push(`${doc.frontmatter.id} (${issue2.key})`);
|
|
17850
|
+
}
|
|
17851
|
+
}
|
|
17852
|
+
const parts = [
|
|
17853
|
+
`Fetched ${result.issues.length} of ${result.total} matching issues.`
|
|
17854
|
+
];
|
|
17855
|
+
if (created.length > 0) parts.push(`Created: ${created.join(", ")}`);
|
|
17856
|
+
if (updated.length > 0) parts.push(`Updated: ${updated.join(", ")}`);
|
|
17857
|
+
return {
|
|
17858
|
+
content: [{ type: "text", text: parts.join("\n") }]
|
|
17859
|
+
};
|
|
17860
|
+
}
|
|
17861
|
+
),
|
|
17862
|
+
// --- Local → Jira tools ---
|
|
17863
|
+
tool19(
|
|
17864
|
+
"push_artifact_to_jira",
|
|
17865
|
+
"Create a Jira issue from any Marvin artifact (D/A/Q/F/E) and create a tracking JI-xxx document",
|
|
17866
|
+
{
|
|
17867
|
+
artifactId: external_exports.string().describe("Marvin artifact ID (e.g. 'D-001', 'F-003', 'E-002')"),
|
|
17868
|
+
projectKey: external_exports.string().describe("Jira project key (e.g. 'PROJ')"),
|
|
17869
|
+
issueType: external_exports.enum(["Story", "Task", "Bug", "Epic"]).optional().describe("Jira issue type (default: 'Task')")
|
|
17870
|
+
},
|
|
17871
|
+
async (args) => {
|
|
17872
|
+
const client = createJiraClient();
|
|
17873
|
+
if (!client) return jiraNotConfiguredError();
|
|
17874
|
+
const artifact = store.get(args.artifactId);
|
|
17875
|
+
if (!artifact) {
|
|
17876
|
+
return {
|
|
17877
|
+
content: [
|
|
17878
|
+
{ type: "text", text: `Artifact ${args.artifactId} not found` }
|
|
17879
|
+
],
|
|
17880
|
+
isError: true
|
|
17881
|
+
};
|
|
17882
|
+
}
|
|
17883
|
+
const description = [
|
|
17884
|
+
artifact.content,
|
|
17885
|
+
"",
|
|
17886
|
+
`---`,
|
|
17887
|
+
`Marvin artifact: ${artifact.frontmatter.id} (${artifact.frontmatter.type})`,
|
|
17888
|
+
`Status: ${artifact.frontmatter.status}`
|
|
17889
|
+
].join("\n");
|
|
17890
|
+
const jiraResult = await client.createIssue({
|
|
17891
|
+
project: { key: args.projectKey },
|
|
17892
|
+
summary: artifact.frontmatter.title,
|
|
17893
|
+
description,
|
|
17894
|
+
issuetype: { name: args.issueType ?? "Task" }
|
|
17895
|
+
});
|
|
17896
|
+
const host = process.env.JIRA_HOST;
|
|
17897
|
+
const jiDoc = store.create(
|
|
17898
|
+
JIRA_TYPE,
|
|
17899
|
+
{
|
|
17900
|
+
title: artifact.frontmatter.title,
|
|
17901
|
+
status: "open",
|
|
17902
|
+
jiraKey: jiraResult.key,
|
|
17903
|
+
jiraUrl: `https://${host}/browse/${jiraResult.key}`,
|
|
17904
|
+
issueType: args.issueType ?? "Task",
|
|
17905
|
+
priority: "Medium",
|
|
17906
|
+
assignee: "",
|
|
17907
|
+
labels: [],
|
|
17908
|
+
linkedArtifacts: [args.artifactId],
|
|
17909
|
+
tags: [`jira:${jiraResult.key}`],
|
|
17910
|
+
lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
17911
|
+
},
|
|
17912
|
+
""
|
|
17913
|
+
);
|
|
17914
|
+
return {
|
|
17915
|
+
content: [
|
|
17916
|
+
{
|
|
17917
|
+
type: "text",
|
|
17918
|
+
text: `Created Jira ${jiraResult.key} from ${args.artifactId}. Tracking locally as ${jiDoc.frontmatter.id}.`
|
|
17919
|
+
}
|
|
17920
|
+
]
|
|
17921
|
+
};
|
|
17922
|
+
}
|
|
17923
|
+
),
|
|
17924
|
+
// --- Bidirectional sync ---
|
|
17925
|
+
tool19(
|
|
17926
|
+
"sync_jira_issue",
|
|
17927
|
+
"Bidirectional sync: push local title/description to Jira, pull latest status/assignee/labels back",
|
|
17928
|
+
{
|
|
17929
|
+
id: external_exports.string().describe("Local JI-xxx ID")
|
|
17930
|
+
},
|
|
17931
|
+
async (args) => {
|
|
17932
|
+
const client = createJiraClient();
|
|
17933
|
+
if (!client) return jiraNotConfiguredError();
|
|
17934
|
+
const doc = store.get(args.id);
|
|
17935
|
+
if (!doc || doc.frontmatter.type !== JIRA_TYPE) {
|
|
17936
|
+
return {
|
|
17937
|
+
content: [
|
|
17938
|
+
{ type: "text", text: `Jira issue ${args.id} not found locally` }
|
|
17939
|
+
],
|
|
17940
|
+
isError: true
|
|
17941
|
+
};
|
|
17942
|
+
}
|
|
17943
|
+
const jiraKey = doc.frontmatter.jiraKey;
|
|
17944
|
+
await client.updateIssue(jiraKey, {
|
|
17945
|
+
summary: doc.frontmatter.title,
|
|
17946
|
+
description: doc.content || void 0
|
|
17947
|
+
});
|
|
17948
|
+
const issue2 = await client.getIssue(jiraKey);
|
|
17949
|
+
const host = process.env.JIRA_HOST;
|
|
17950
|
+
const fm = jiraIssueToFrontmatter(
|
|
17951
|
+
issue2,
|
|
17952
|
+
host,
|
|
17953
|
+
doc.frontmatter.linkedArtifacts
|
|
17954
|
+
);
|
|
17955
|
+
store.update(args.id, fm, issue2.fields.description ?? "");
|
|
17956
|
+
return {
|
|
17957
|
+
content: [
|
|
17958
|
+
{
|
|
17959
|
+
type: "text",
|
|
17960
|
+
text: `Synced ${args.id} \u2194 ${jiraKey}. Status: ${fm.status}, Assignee: ${fm.assignee || "unassigned"}`
|
|
17961
|
+
}
|
|
17962
|
+
]
|
|
17963
|
+
};
|
|
17964
|
+
}
|
|
17965
|
+
),
|
|
17966
|
+
// --- Local link tool ---
|
|
17967
|
+
tool19(
|
|
17968
|
+
"link_artifact_to_jira",
|
|
17969
|
+
"Add a Marvin artifact ID to a JI-xxx document's linkedArtifacts field",
|
|
17970
|
+
{
|
|
17971
|
+
jiraIssueId: external_exports.string().describe("Local JI-xxx ID"),
|
|
17972
|
+
artifactId: external_exports.string().describe("Marvin artifact ID to link (e.g. 'D-001', 'F-003')")
|
|
17973
|
+
},
|
|
17974
|
+
async (args) => {
|
|
17975
|
+
const doc = store.get(args.jiraIssueId);
|
|
17976
|
+
if (!doc || doc.frontmatter.type !== JIRA_TYPE) {
|
|
17977
|
+
return {
|
|
17978
|
+
content: [
|
|
17979
|
+
{
|
|
17980
|
+
type: "text",
|
|
17981
|
+
text: `Jira issue ${args.jiraIssueId} not found locally`
|
|
17982
|
+
}
|
|
17983
|
+
],
|
|
17984
|
+
isError: true
|
|
17985
|
+
};
|
|
17986
|
+
}
|
|
17987
|
+
const artifact = store.get(args.artifactId);
|
|
17988
|
+
if (!artifact) {
|
|
17989
|
+
return {
|
|
17990
|
+
content: [
|
|
17991
|
+
{ type: "text", text: `Artifact ${args.artifactId} not found` }
|
|
17992
|
+
],
|
|
17993
|
+
isError: true
|
|
17994
|
+
};
|
|
17995
|
+
}
|
|
17996
|
+
const linked = doc.frontmatter.linkedArtifacts ?? [];
|
|
17997
|
+
if (linked.includes(args.artifactId)) {
|
|
17998
|
+
return {
|
|
17999
|
+
content: [
|
|
18000
|
+
{
|
|
18001
|
+
type: "text",
|
|
18002
|
+
text: `${args.artifactId} is already linked to ${args.jiraIssueId}`
|
|
18003
|
+
}
|
|
18004
|
+
]
|
|
18005
|
+
};
|
|
18006
|
+
}
|
|
18007
|
+
store.update(args.jiraIssueId, {
|
|
18008
|
+
linkedArtifacts: [...linked, args.artifactId]
|
|
18009
|
+
});
|
|
18010
|
+
return {
|
|
18011
|
+
content: [
|
|
18012
|
+
{
|
|
18013
|
+
type: "text",
|
|
18014
|
+
text: `Linked ${args.artifactId} to ${args.jiraIssueId}`
|
|
18015
|
+
}
|
|
18016
|
+
]
|
|
18017
|
+
};
|
|
18018
|
+
}
|
|
18019
|
+
)
|
|
18020
|
+
];
|
|
18021
|
+
}
|
|
18022
|
+
|
|
18023
|
+
// src/skills/builtin/jira/index.ts
|
|
18024
|
+
var jiraSkill = {
|
|
18025
|
+
id: "jira",
|
|
18026
|
+
name: "Jira Integration",
|
|
18027
|
+
description: "Bidirectional sync between Marvin artifacts and Jira issues",
|
|
18028
|
+
version: "1.0.0",
|
|
18029
|
+
format: "builtin-ts",
|
|
18030
|
+
// No default persona affinity — opt-in via config.yaml skills section
|
|
18031
|
+
documentTypeRegistrations: [
|
|
18032
|
+
{ type: "jira-issue", dirName: "jira-issues", idPrefix: "JI" }
|
|
18033
|
+
],
|
|
18034
|
+
tools: (store) => createJiraTools(store),
|
|
18035
|
+
promptFragments: {
|
|
18036
|
+
"product-owner": `You have the **Jira Integration** skill. You can pull issues from Jira and push Marvin artifacts to Jira.
|
|
18037
|
+
|
|
18038
|
+
**Available tools:**
|
|
18039
|
+
- \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues
|
|
18040
|
+
- \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import issues from Jira by key or JQL query
|
|
18041
|
+
- \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, feature, etc.)
|
|
18042
|
+
- \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
|
|
18043
|
+
- \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx
|
|
18044
|
+
|
|
18045
|
+
**As Product Owner, use Jira integration to:**
|
|
18046
|
+
- Pull stakeholder-reported issues for triage and prioritization
|
|
18047
|
+
- Push approved features as Stories for development tracking
|
|
18048
|
+
- Link decisions to Jira issues for audit trail and traceability
|
|
18049
|
+
- Use JQL queries to review backlog status (e.g. \`project = PROJ AND status = "To Do"\`)`,
|
|
18050
|
+
"tech-lead": `You have the **Jira Integration** skill. You can pull issues from Jira and push Marvin artifacts to Jira.
|
|
18051
|
+
|
|
18052
|
+
**Available tools:**
|
|
18053
|
+
- \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues
|
|
18054
|
+
- \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import issues from Jira by key or JQL query
|
|
18055
|
+
- \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, action, epic, etc.)
|
|
18056
|
+
- \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
|
|
18057
|
+
- \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx
|
|
18058
|
+
|
|
18059
|
+
**As Tech Lead, use Jira integration to:**
|
|
18060
|
+
- Pull technical issues and bugs for sprint planning and estimation
|
|
18061
|
+
- Push epics and technical decisions to Jira for cross-team visibility
|
|
18062
|
+
- Bidirectional sync to keep local governance and Jira in alignment
|
|
18063
|
+
- Use JQL queries to track technical debt (e.g. \`labels = "tech-debt" AND status != "Done"\`)`,
|
|
18064
|
+
"delivery-manager": `You have the **Jira Integration** skill. You can pull issues from Jira and push Marvin artifacts to Jira.
|
|
18065
|
+
|
|
18066
|
+
**Available tools:**
|
|
18067
|
+
- \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues
|
|
18068
|
+
- \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import issues from Jira by key or JQL query
|
|
18069
|
+
- \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, action, etc.)
|
|
18070
|
+
- \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
|
|
18071
|
+
- \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx
|
|
18072
|
+
|
|
18073
|
+
**As Delivery Manager, use Jira integration to:**
|
|
18074
|
+
- Pull sprint issues for tracking progress and blockers
|
|
18075
|
+
- Push actions and decisions to Jira for stakeholder visibility
|
|
18076
|
+
- Use JQL queries for reporting (e.g. \`sprint in openSprints() AND assignee = currentUser()\`)
|
|
18077
|
+
- Sync status between Marvin governance items and Jira issues`
|
|
18078
|
+
}
|
|
18079
|
+
};
|
|
18080
|
+
|
|
17311
18081
|
// src/skills/registry.ts
|
|
17312
18082
|
var BUILTIN_SKILLS = {
|
|
17313
|
-
"governance-review": governanceReviewSkill
|
|
18083
|
+
"governance-review": governanceReviewSkill,
|
|
18084
|
+
"jira": jiraSkill
|
|
17314
18085
|
};
|
|
17315
18086
|
var GOVERNANCE_TOOL_NAMES = [
|
|
17316
18087
|
"mcp__marvin-governance__list_decisions",
|
|
@@ -17459,6 +18230,16 @@ function resolveSkillsForPersona(personaId, skillsConfig, allSkills) {
|
|
|
17459
18230
|
}
|
|
17460
18231
|
return result;
|
|
17461
18232
|
}
|
|
18233
|
+
function collectSkillRegistrations(skillIds, allSkills) {
|
|
18234
|
+
const registrations = [];
|
|
18235
|
+
for (const id of skillIds) {
|
|
18236
|
+
const skill = allSkills.get(id);
|
|
18237
|
+
if (skill?.documentTypeRegistrations) {
|
|
18238
|
+
registrations.push(...skill.documentTypeRegistrations);
|
|
18239
|
+
}
|
|
18240
|
+
}
|
|
18241
|
+
return registrations;
|
|
18242
|
+
}
|
|
17462
18243
|
function getSkillTools(skillIds, allSkills, store) {
|
|
17463
18244
|
const tools = [];
|
|
17464
18245
|
for (const id of skillIds) {
|
|
@@ -17570,16 +18351,17 @@ ${wildcardPrompt}
|
|
|
17570
18351
|
async function startSession(options) {
|
|
17571
18352
|
const { persona, config: config2, marvinDir, projectRoot } = options;
|
|
17572
18353
|
const plugin = resolvePlugin(config2.project.methodology);
|
|
17573
|
-
const
|
|
17574
|
-
const
|
|
18354
|
+
const pluginRegistrations = plugin?.documentTypeRegistrations ?? [];
|
|
18355
|
+
const allSkills = loadAllSkills(marvinDir);
|
|
18356
|
+
const skillIds = resolveSkillsForPersona(persona.id, config2.project.skills, allSkills);
|
|
18357
|
+
const skillRegistrations = collectSkillRegistrations(skillIds, allSkills);
|
|
18358
|
+
const store = new DocumentStore(marvinDir, [...pluginRegistrations, ...skillRegistrations]);
|
|
17575
18359
|
const sessionStore = new SessionStore(marvinDir);
|
|
17576
18360
|
const sourcesDir = path8.join(marvinDir, "sources");
|
|
17577
18361
|
const hasSourcesDir = fs8.existsSync(sourcesDir);
|
|
17578
18362
|
const manifest = hasSourcesDir ? new SourceManifestManager(marvinDir) : void 0;
|
|
17579
18363
|
const pluginTools = plugin ? getPluginTools(plugin, store, marvinDir) : [];
|
|
17580
18364
|
const pluginPromptFragment = plugin ? getPluginPromptFragment(plugin, persona.id) : void 0;
|
|
17581
|
-
const allSkills = loadAllSkills(marvinDir);
|
|
17582
|
-
const skillIds = resolveSkillsForPersona(persona.id, config2.project.skills, allSkills);
|
|
17583
18365
|
const codeSkillTools = getSkillTools(skillIds, allSkills, store);
|
|
17584
18366
|
const skillAgents = getSkillAgentDefinitions(skillIds, allSkills);
|
|
17585
18367
|
const skillPromptFragment = getSkillPromptFragment(skillIds, allSkills, persona.id);
|
|
@@ -17776,7 +18558,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
17776
18558
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
17777
18559
|
|
|
17778
18560
|
// src/skills/action-tools.ts
|
|
17779
|
-
import { tool as
|
|
18561
|
+
import { tool as tool20 } from "@anthropic-ai/claude-agent-sdk";
|
|
17780
18562
|
|
|
17781
18563
|
// src/skills/action-runner.ts
|
|
17782
18564
|
import { query as query3 } from "@anthropic-ai/claude-agent-sdk";
|
|
@@ -17842,7 +18624,7 @@ function createSkillActionTools(skills, context) {
|
|
|
17842
18624
|
if (!skill.actions) continue;
|
|
17843
18625
|
for (const action of skill.actions) {
|
|
17844
18626
|
tools.push(
|
|
17845
|
-
|
|
18627
|
+
tool20(
|
|
17846
18628
|
`${skill.id}__${action.id}`,
|
|
17847
18629
|
action.description,
|
|
17848
18630
|
{
|
|
@@ -17934,10 +18716,10 @@ ${lines.join("\n\n")}`;
|
|
|
17934
18716
|
}
|
|
17935
18717
|
|
|
17936
18718
|
// src/mcp/persona-tools.ts
|
|
17937
|
-
import { tool as
|
|
18719
|
+
import { tool as tool21 } from "@anthropic-ai/claude-agent-sdk";
|
|
17938
18720
|
function createPersonaTools(ctx, marvinDir) {
|
|
17939
18721
|
return [
|
|
17940
|
-
|
|
18722
|
+
tool21(
|
|
17941
18723
|
"set_persona",
|
|
17942
18724
|
"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.",
|
|
17943
18725
|
{
|
|
@@ -17967,7 +18749,7 @@ ${summaries}`
|
|
|
17967
18749
|
};
|
|
17968
18750
|
}
|
|
17969
18751
|
),
|
|
17970
|
-
|
|
18752
|
+
tool21(
|
|
17971
18753
|
"get_persona_guidance",
|
|
17972
18754
|
"Get guidance for a persona without changing the active persona. If no persona is specified, lists all available personas with summaries.",
|
|
17973
18755
|
{
|
|
@@ -19210,20 +19992,23 @@ async function skillsInstallCommand(skillId, options) {
|
|
|
19210
19992
|
console.log(chalk10.red("Please specify a persona with --as <persona>."));
|
|
19211
19993
|
return;
|
|
19212
19994
|
}
|
|
19995
|
+
const targets = persona === "all" ? listPersonas().map((p) => p.id) : [persona];
|
|
19213
19996
|
const config2 = loadProjectConfig(project.marvinDir);
|
|
19214
19997
|
if (!config2.skills) {
|
|
19215
19998
|
config2.skills = {};
|
|
19216
19999
|
}
|
|
19217
|
-
|
|
19218
|
-
config2.skills[
|
|
19219
|
-
|
|
19220
|
-
|
|
19221
|
-
|
|
19222
|
-
|
|
20000
|
+
for (const target of targets) {
|
|
20001
|
+
if (!config2.skills[target]) {
|
|
20002
|
+
config2.skills[target] = [];
|
|
20003
|
+
}
|
|
20004
|
+
if (config2.skills[target].includes(skillId)) {
|
|
20005
|
+
console.log(chalk10.yellow(`Skill "${skillId}" is already assigned to ${target}.`));
|
|
20006
|
+
continue;
|
|
20007
|
+
}
|
|
20008
|
+
config2.skills[target].push(skillId);
|
|
20009
|
+
console.log(chalk10.green(`Assigned skill "${skillId}" to ${target}.`));
|
|
19223
20010
|
}
|
|
19224
|
-
config2.skills[persona].push(skillId);
|
|
19225
20011
|
saveProjectConfig(project.marvinDir, config2);
|
|
19226
|
-
console.log(chalk10.green(`Assigned skill "${skillId}" to ${persona}.`));
|
|
19227
20012
|
}
|
|
19228
20013
|
async function skillsRemoveCommand(skillId, options) {
|
|
19229
20014
|
const project = loadProject();
|
|
@@ -19232,25 +20017,28 @@ async function skillsRemoveCommand(skillId, options) {
|
|
|
19232
20017
|
console.log(chalk10.red("Please specify a persona with --as <persona>."));
|
|
19233
20018
|
return;
|
|
19234
20019
|
}
|
|
20020
|
+
const targets = persona === "all" ? listPersonas().map((p) => p.id) : [persona];
|
|
19235
20021
|
const config2 = loadProjectConfig(project.marvinDir);
|
|
19236
|
-
|
|
19237
|
-
|
|
19238
|
-
|
|
19239
|
-
|
|
19240
|
-
|
|
19241
|
-
|
|
19242
|
-
|
|
19243
|
-
|
|
19244
|
-
|
|
19245
|
-
|
|
19246
|
-
|
|
19247
|
-
|
|
20022
|
+
for (const target of targets) {
|
|
20023
|
+
if (!config2.skills?.[target]) {
|
|
20024
|
+
console.log(chalk10.yellow(`No skills configured for ${target}.`));
|
|
20025
|
+
continue;
|
|
20026
|
+
}
|
|
20027
|
+
const idx = config2.skills[target].indexOf(skillId);
|
|
20028
|
+
if (idx === -1) {
|
|
20029
|
+
console.log(chalk10.yellow(`Skill "${skillId}" is not assigned to ${target}.`));
|
|
20030
|
+
continue;
|
|
20031
|
+
}
|
|
20032
|
+
config2.skills[target].splice(idx, 1);
|
|
20033
|
+
if (config2.skills[target].length === 0) {
|
|
20034
|
+
delete config2.skills[target];
|
|
20035
|
+
}
|
|
20036
|
+
console.log(chalk10.green(`Removed skill "${skillId}" from ${target}.`));
|
|
19248
20037
|
}
|
|
19249
|
-
if (Object.keys(config2.skills).length === 0) {
|
|
20038
|
+
if (config2.skills && Object.keys(config2.skills).length === 0) {
|
|
19250
20039
|
delete config2.skills;
|
|
19251
20040
|
}
|
|
19252
20041
|
saveProjectConfig(project.marvinDir, config2);
|
|
19253
|
-
console.log(chalk10.green(`Removed skill "${skillId}" from ${persona}.`));
|
|
19254
20042
|
}
|
|
19255
20043
|
async function skillsCreateCommand(name) {
|
|
19256
20044
|
const project = loadProject();
|
|
@@ -20497,12 +21285,94 @@ Contribution: ${options.type}`));
|
|
|
20497
21285
|
});
|
|
20498
21286
|
}
|
|
20499
21287
|
|
|
21288
|
+
// src/reports/gar/render-ascii.ts
|
|
21289
|
+
import chalk16 from "chalk";
|
|
21290
|
+
var STATUS_DOT = {
|
|
21291
|
+
green: chalk16.green("\u25CF"),
|
|
21292
|
+
amber: chalk16.yellow("\u25CF"),
|
|
21293
|
+
red: chalk16.red("\u25CF")
|
|
21294
|
+
};
|
|
21295
|
+
var STATUS_LABEL = {
|
|
21296
|
+
green: chalk16.green.bold("GREEN"),
|
|
21297
|
+
amber: chalk16.yellow.bold("AMBER"),
|
|
21298
|
+
red: chalk16.red.bold("RED")
|
|
21299
|
+
};
|
|
21300
|
+
var SEPARATOR = chalk16.dim("\u2500".repeat(60));
|
|
21301
|
+
function renderAscii(report) {
|
|
21302
|
+
const lines = [];
|
|
21303
|
+
lines.push("");
|
|
21304
|
+
lines.push(chalk16.bold(` GAR Report \xB7 ${report.projectName}`));
|
|
21305
|
+
lines.push(chalk16.dim(` ${report.generatedAt}`));
|
|
21306
|
+
lines.push("");
|
|
21307
|
+
lines.push(` Overall: ${STATUS_LABEL[report.overall]}`);
|
|
21308
|
+
lines.push("");
|
|
21309
|
+
lines.push(` ${SEPARATOR}`);
|
|
21310
|
+
for (const area of report.areas) {
|
|
21311
|
+
lines.push(` ${STATUS_DOT[area.status]} ${chalk16.bold(area.name.padEnd(12))} ${area.summary}`);
|
|
21312
|
+
for (const item of area.items) {
|
|
21313
|
+
lines.push(` ${chalk16.dim("\u2514")} ${item.id} ${item.title}`);
|
|
21314
|
+
}
|
|
21315
|
+
}
|
|
21316
|
+
lines.push(` ${SEPARATOR}`);
|
|
21317
|
+
lines.push("");
|
|
21318
|
+
return lines.join("\n");
|
|
21319
|
+
}
|
|
21320
|
+
|
|
21321
|
+
// src/reports/gar/render-confluence.ts
|
|
21322
|
+
var EMOJI = {
|
|
21323
|
+
green: ":green_circle:",
|
|
21324
|
+
amber: ":yellow_circle:",
|
|
21325
|
+
red: ":red_circle:"
|
|
21326
|
+
};
|
|
21327
|
+
function renderConfluence(report) {
|
|
21328
|
+
const lines = [];
|
|
21329
|
+
lines.push(`# GAR Report \u2014 ${report.projectName}`);
|
|
21330
|
+
lines.push("");
|
|
21331
|
+
lines.push(`**Date:** ${report.generatedAt}`);
|
|
21332
|
+
lines.push(`**Overall:** ${EMOJI[report.overall]} ${report.overall.toUpperCase()}`);
|
|
21333
|
+
lines.push("");
|
|
21334
|
+
lines.push("| Area | Status | Summary |");
|
|
21335
|
+
lines.push("|------|--------|---------|");
|
|
21336
|
+
for (const area of report.areas) {
|
|
21337
|
+
lines.push(
|
|
21338
|
+
`| ${area.name} | ${EMOJI[area.status]} ${area.status.toUpperCase()} | ${area.summary} |`
|
|
21339
|
+
);
|
|
21340
|
+
}
|
|
21341
|
+
lines.push("");
|
|
21342
|
+
for (const area of report.areas) {
|
|
21343
|
+
if (area.items.length === 0) continue;
|
|
21344
|
+
lines.push(`## ${area.name}`);
|
|
21345
|
+
lines.push("");
|
|
21346
|
+
for (const item of area.items) {
|
|
21347
|
+
lines.push(`- **${item.id}** ${item.title}`);
|
|
21348
|
+
}
|
|
21349
|
+
lines.push("");
|
|
21350
|
+
}
|
|
21351
|
+
return lines.join("\n");
|
|
21352
|
+
}
|
|
21353
|
+
|
|
21354
|
+
// src/cli/commands/report.ts
|
|
21355
|
+
async function garReportCommand(options) {
|
|
21356
|
+
const project = loadProject();
|
|
21357
|
+
const plugin = resolvePlugin(project.config.methodology);
|
|
21358
|
+
const registrations = plugin?.documentTypeRegistrations ?? [];
|
|
21359
|
+
const store = new DocumentStore(project.marvinDir, registrations);
|
|
21360
|
+
const metrics = collectGarMetrics(store);
|
|
21361
|
+
const report = evaluateGar(project.config.name, metrics);
|
|
21362
|
+
const format = options.format ?? "ascii";
|
|
21363
|
+
if (format === "confluence") {
|
|
21364
|
+
console.log(renderConfluence(report));
|
|
21365
|
+
} else {
|
|
21366
|
+
console.log(renderAscii(report));
|
|
21367
|
+
}
|
|
21368
|
+
}
|
|
21369
|
+
|
|
20500
21370
|
// src/cli/program.ts
|
|
20501
21371
|
function createProgram() {
|
|
20502
21372
|
const program = new Command();
|
|
20503
21373
|
program.name("marvin").description(
|
|
20504
21374
|
"AI-powered product development assistant with Product Owner, Delivery Manager, and Technical Lead personas"
|
|
20505
|
-
).version("0.2.
|
|
21375
|
+
).version("0.2.8");
|
|
20506
21376
|
program.command("init").description("Initialize a new Marvin project in the current directory").action(async () => {
|
|
20507
21377
|
await initCommand();
|
|
20508
21378
|
});
|
|
@@ -20572,6 +21442,13 @@ function createProgram() {
|
|
|
20572
21442
|
skillsCmd.command("migrate").description("Migrate YAML skill files to SKILL.md directory format").action(async () => {
|
|
20573
21443
|
await skillsMigrateCommand();
|
|
20574
21444
|
});
|
|
21445
|
+
const reportCmd = program.command("report").description("Generate project reports");
|
|
21446
|
+
reportCmd.command("gar").description("Generate a Green/Amber/Red status report").option(
|
|
21447
|
+
"--format <format>",
|
|
21448
|
+
"Output format: ascii or confluence (default: ascii)"
|
|
21449
|
+
).action(async (options) => {
|
|
21450
|
+
await garReportCommand(options);
|
|
21451
|
+
});
|
|
20575
21452
|
return program;
|
|
20576
21453
|
}
|
|
20577
21454
|
export {
|