mrvn-cli 0.2.6 → 0.2.9
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 +89 -3
- package/dist/index.d.ts +8 -1
- package/dist/index.js +968 -254
- package/dist/index.js.map +1 -1
- package/dist/marvin-serve.js +637 -46
- package/dist/marvin-serve.js.map +1 -1
- package/dist/marvin.js +968 -254
- package/dist/marvin.js.map +1 -1
- package/package.json +1 -1
package/dist/marvin-serve.js
CHANGED
|
@@ -39,6 +39,26 @@ import * as fs from "fs";
|
|
|
39
39
|
import * as path from "path";
|
|
40
40
|
import * as os from "os";
|
|
41
41
|
import * as YAML from "yaml";
|
|
42
|
+
function userConfigDir() {
|
|
43
|
+
return path.join(os.homedir(), ".config", "marvin");
|
|
44
|
+
}
|
|
45
|
+
function userConfigPath() {
|
|
46
|
+
return path.join(userConfigDir(), "config.yaml");
|
|
47
|
+
}
|
|
48
|
+
function loadUserConfig() {
|
|
49
|
+
const configPath = userConfigPath();
|
|
50
|
+
if (!fs.existsSync(configPath)) {
|
|
51
|
+
return {};
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
const raw = fs.readFileSync(configPath, "utf-8");
|
|
55
|
+
return YAML.parse(raw) ?? {};
|
|
56
|
+
} catch (err) {
|
|
57
|
+
throw new ConfigError(
|
|
58
|
+
`Failed to parse user config at ${configPath}: ${err}`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
42
62
|
function loadProjectConfig(marvinDir) {
|
|
43
63
|
const configPath = path.join(marvinDir, "config.yaml");
|
|
44
64
|
if (!fs.existsSync(configPath)) {
|
|
@@ -266,13 +286,10 @@ var DocumentStore = class {
|
|
|
266
286
|
if (!prefix) {
|
|
267
287
|
throw new Error(`Unknown document type: ${type}`);
|
|
268
288
|
}
|
|
269
|
-
const
|
|
270
|
-
const dir = path3.join(this.docsDir, dirName);
|
|
271
|
-
if (!fs3.existsSync(dir)) return `${prefix}-001`;
|
|
272
|
-
const files = fs3.readdirSync(dir).filter((f) => f.endsWith(".md"));
|
|
289
|
+
const pattern = new RegExp(`^${prefix}-(\\d+)$`);
|
|
273
290
|
let maxNum = 0;
|
|
274
|
-
for (const
|
|
275
|
-
const match =
|
|
291
|
+
for (const id of this.index.keys()) {
|
|
292
|
+
const match = id.match(pattern);
|
|
276
293
|
if (match) {
|
|
277
294
|
maxNum = Math.max(maxNum, parseInt(match[1], 10));
|
|
278
295
|
}
|
|
@@ -14971,6 +14988,128 @@ function createMeetingTools(store) {
|
|
|
14971
14988
|
|
|
14972
14989
|
// src/plugins/builtin/tools/reports.ts
|
|
14973
14990
|
import { tool as tool8 } from "@anthropic-ai/claude-agent-sdk";
|
|
14991
|
+
|
|
14992
|
+
// src/reports/gar/collector.ts
|
|
14993
|
+
function collectGarMetrics(store) {
|
|
14994
|
+
const allActions = store.list({ type: "action" });
|
|
14995
|
+
const openActions = allActions.filter((d) => d.frontmatter.status === "open");
|
|
14996
|
+
const doneActions = allActions.filter((d) => d.frontmatter.status === "done");
|
|
14997
|
+
const allDocs = store.list();
|
|
14998
|
+
const blockedItems = allDocs.filter(
|
|
14999
|
+
(d) => d.frontmatter.tags?.includes("blocked")
|
|
15000
|
+
);
|
|
15001
|
+
const overdueItems = allDocs.filter(
|
|
15002
|
+
(d) => d.frontmatter.tags?.includes("overdue")
|
|
15003
|
+
);
|
|
15004
|
+
const openQuestions = store.list({ type: "question", status: "open" });
|
|
15005
|
+
const riskItems = allDocs.filter(
|
|
15006
|
+
(d) => d.frontmatter.tags?.includes("risk")
|
|
15007
|
+
);
|
|
15008
|
+
const unownedActions = openActions.filter((d) => !d.frontmatter.owner);
|
|
15009
|
+
const total = allActions.length;
|
|
15010
|
+
const done = doneActions.length;
|
|
15011
|
+
const completionPct = total > 0 ? Math.round(done / total * 100) : 100;
|
|
15012
|
+
const scheduleItems = [
|
|
15013
|
+
...blockedItems,
|
|
15014
|
+
...overdueItems
|
|
15015
|
+
].filter(
|
|
15016
|
+
(d, i, arr) => arr.findIndex((x) => x.frontmatter.id === d.frontmatter.id) === i
|
|
15017
|
+
).map((d) => ({ id: d.frontmatter.id, title: d.frontmatter.title }));
|
|
15018
|
+
const qualityItems = [
|
|
15019
|
+
...riskItems,
|
|
15020
|
+
...openQuestions
|
|
15021
|
+
].filter(
|
|
15022
|
+
(d, i, arr) => arr.findIndex((x) => x.frontmatter.id === d.frontmatter.id) === i
|
|
15023
|
+
).map((d) => ({ id: d.frontmatter.id, title: d.frontmatter.title }));
|
|
15024
|
+
const resourceItems = unownedActions.map((d) => ({
|
|
15025
|
+
id: d.frontmatter.id,
|
|
15026
|
+
title: d.frontmatter.title
|
|
15027
|
+
}));
|
|
15028
|
+
return {
|
|
15029
|
+
scope: {
|
|
15030
|
+
total,
|
|
15031
|
+
open: openActions.length,
|
|
15032
|
+
done,
|
|
15033
|
+
completionPct
|
|
15034
|
+
},
|
|
15035
|
+
schedule: {
|
|
15036
|
+
blocked: blockedItems.length,
|
|
15037
|
+
overdue: overdueItems.length,
|
|
15038
|
+
items: scheduleItems
|
|
15039
|
+
},
|
|
15040
|
+
quality: {
|
|
15041
|
+
risks: riskItems.length,
|
|
15042
|
+
openQuestions: openQuestions.length,
|
|
15043
|
+
items: qualityItems
|
|
15044
|
+
},
|
|
15045
|
+
resources: {
|
|
15046
|
+
unowned: unownedActions.length,
|
|
15047
|
+
items: resourceItems
|
|
15048
|
+
}
|
|
15049
|
+
};
|
|
15050
|
+
}
|
|
15051
|
+
|
|
15052
|
+
// src/reports/gar/evaluator.ts
|
|
15053
|
+
function worstStatus(statuses) {
|
|
15054
|
+
if (statuses.includes("red")) return "red";
|
|
15055
|
+
if (statuses.includes("amber")) return "amber";
|
|
15056
|
+
return "green";
|
|
15057
|
+
}
|
|
15058
|
+
function evaluateGar(projectName, metrics) {
|
|
15059
|
+
const areas = [];
|
|
15060
|
+
const scopePct = metrics.scope.completionPct;
|
|
15061
|
+
const scopeStatus = scopePct >= 70 ? "green" : scopePct >= 40 ? "amber" : "red";
|
|
15062
|
+
areas.push({
|
|
15063
|
+
name: "Scope",
|
|
15064
|
+
status: scopeStatus,
|
|
15065
|
+
summary: `${scopePct}% complete (${metrics.scope.done}/${metrics.scope.total})`,
|
|
15066
|
+
items: []
|
|
15067
|
+
});
|
|
15068
|
+
const scheduleCount = metrics.schedule.blocked + metrics.schedule.overdue;
|
|
15069
|
+
const scheduleStatus = scheduleCount === 0 ? "green" : scheduleCount <= 2 ? "amber" : "red";
|
|
15070
|
+
const scheduleParts = [];
|
|
15071
|
+
if (metrics.schedule.blocked > 0)
|
|
15072
|
+
scheduleParts.push(`${metrics.schedule.blocked} blocked`);
|
|
15073
|
+
if (metrics.schedule.overdue > 0)
|
|
15074
|
+
scheduleParts.push(`${metrics.schedule.overdue} overdue`);
|
|
15075
|
+
areas.push({
|
|
15076
|
+
name: "Schedule",
|
|
15077
|
+
status: scheduleStatus,
|
|
15078
|
+
summary: scheduleParts.length > 0 ? scheduleParts.join(", ") : "on track",
|
|
15079
|
+
items: metrics.schedule.items
|
|
15080
|
+
});
|
|
15081
|
+
const qualityCount = metrics.quality.risks + metrics.quality.openQuestions;
|
|
15082
|
+
const qualityStatus = qualityCount === 0 ? "green" : qualityCount <= 2 ? "amber" : "red";
|
|
15083
|
+
const qualityParts = [];
|
|
15084
|
+
if (metrics.quality.risks > 0)
|
|
15085
|
+
qualityParts.push(`${metrics.quality.risks} risk(s)`);
|
|
15086
|
+
if (metrics.quality.openQuestions > 0)
|
|
15087
|
+
qualityParts.push(`${metrics.quality.openQuestions} open question(s)`);
|
|
15088
|
+
areas.push({
|
|
15089
|
+
name: "Quality",
|
|
15090
|
+
status: qualityStatus,
|
|
15091
|
+
summary: qualityParts.length > 0 ? qualityParts.join(", ") : "no issues",
|
|
15092
|
+
items: metrics.quality.items
|
|
15093
|
+
});
|
|
15094
|
+
const resourceCount = metrics.resources.unowned;
|
|
15095
|
+
const resourceStatus = resourceCount === 0 ? "green" : resourceCount <= 2 ? "amber" : "red";
|
|
15096
|
+
areas.push({
|
|
15097
|
+
name: "Resources",
|
|
15098
|
+
status: resourceStatus,
|
|
15099
|
+
summary: resourceCount > 0 ? `${resourceCount} unowned action(s)` : "all assigned",
|
|
15100
|
+
items: metrics.resources.items
|
|
15101
|
+
});
|
|
15102
|
+
const overall = worstStatus(areas.map((a) => a.status));
|
|
15103
|
+
return {
|
|
15104
|
+
projectName,
|
|
15105
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
15106
|
+
overall,
|
|
15107
|
+
areas,
|
|
15108
|
+
metrics
|
|
15109
|
+
};
|
|
15110
|
+
}
|
|
15111
|
+
|
|
15112
|
+
// src/plugins/builtin/tools/reports.ts
|
|
14974
15113
|
function createReportTools(store) {
|
|
14975
15114
|
return [
|
|
14976
15115
|
tool8(
|
|
@@ -15059,41 +15198,10 @@ function createReportTools(store) {
|
|
|
15059
15198
|
"Generate a Green-Amber-Red report with metrics across scope, schedule, quality, and resources",
|
|
15060
15199
|
{},
|
|
15061
15200
|
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
|
-
};
|
|
15201
|
+
const metrics = collectGarMetrics(store);
|
|
15202
|
+
const report = evaluateGar("project", metrics);
|
|
15095
15203
|
return {
|
|
15096
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
15204
|
+
content: [{ type: "text", text: JSON.stringify(report, null, 2) }]
|
|
15097
15205
|
};
|
|
15098
15206
|
},
|
|
15099
15207
|
{ annotations: { readOnly: true } }
|
|
@@ -17171,9 +17279,492 @@ Be thorough but concise. Focus on actionable insights.`,
|
|
|
17171
17279
|
]
|
|
17172
17280
|
};
|
|
17173
17281
|
|
|
17282
|
+
// src/skills/builtin/jira/tools.ts
|
|
17283
|
+
import { tool as tool19 } from "@anthropic-ai/claude-agent-sdk";
|
|
17284
|
+
|
|
17285
|
+
// src/skills/builtin/jira/client.ts
|
|
17286
|
+
var JiraClient = class {
|
|
17287
|
+
baseUrl;
|
|
17288
|
+
authHeader;
|
|
17289
|
+
constructor(config2) {
|
|
17290
|
+
this.baseUrl = `https://${config2.host}/rest/api/2`;
|
|
17291
|
+
this.authHeader = "Basic " + Buffer.from(`${config2.email}:${config2.apiToken}`).toString("base64");
|
|
17292
|
+
}
|
|
17293
|
+
async request(path10, method = "GET", body) {
|
|
17294
|
+
const url2 = `${this.baseUrl}${path10}`;
|
|
17295
|
+
const headers = {
|
|
17296
|
+
Authorization: this.authHeader,
|
|
17297
|
+
"Content-Type": "application/json",
|
|
17298
|
+
Accept: "application/json"
|
|
17299
|
+
};
|
|
17300
|
+
const response = await fetch(url2, {
|
|
17301
|
+
method,
|
|
17302
|
+
headers,
|
|
17303
|
+
body: body ? JSON.stringify(body) : void 0
|
|
17304
|
+
});
|
|
17305
|
+
if (!response.ok) {
|
|
17306
|
+
const text = await response.text().catch(() => "");
|
|
17307
|
+
throw new Error(
|
|
17308
|
+
`Jira API error ${response.status} ${method} ${path10}: ${text}`
|
|
17309
|
+
);
|
|
17310
|
+
}
|
|
17311
|
+
if (response.status === 204) return void 0;
|
|
17312
|
+
return response.json();
|
|
17313
|
+
}
|
|
17314
|
+
async searchIssues(jql, maxResults = 50) {
|
|
17315
|
+
const params = new URLSearchParams({
|
|
17316
|
+
jql,
|
|
17317
|
+
maxResults: String(maxResults)
|
|
17318
|
+
});
|
|
17319
|
+
return this.request(`/search?${params}`);
|
|
17320
|
+
}
|
|
17321
|
+
async getIssue(key) {
|
|
17322
|
+
return this.request(`/issue/${encodeURIComponent(key)}`);
|
|
17323
|
+
}
|
|
17324
|
+
async createIssue(fields) {
|
|
17325
|
+
return this.request("/issue", "POST", { fields });
|
|
17326
|
+
}
|
|
17327
|
+
async updateIssue(key, fields) {
|
|
17328
|
+
await this.request(
|
|
17329
|
+
`/issue/${encodeURIComponent(key)}`,
|
|
17330
|
+
"PUT",
|
|
17331
|
+
{ fields }
|
|
17332
|
+
);
|
|
17333
|
+
}
|
|
17334
|
+
async addComment(key, body) {
|
|
17335
|
+
await this.request(
|
|
17336
|
+
`/issue/${encodeURIComponent(key)}/comment`,
|
|
17337
|
+
"POST",
|
|
17338
|
+
{ body }
|
|
17339
|
+
);
|
|
17340
|
+
}
|
|
17341
|
+
};
|
|
17342
|
+
function createJiraClient(jiraUserConfig) {
|
|
17343
|
+
const host = jiraUserConfig?.host ?? process.env.JIRA_HOST;
|
|
17344
|
+
const email3 = jiraUserConfig?.email ?? process.env.JIRA_EMAIL;
|
|
17345
|
+
const apiToken = jiraUserConfig?.apiToken ?? process.env.JIRA_API_TOKEN;
|
|
17346
|
+
if (!host || !email3 || !apiToken) return null;
|
|
17347
|
+
return { client: new JiraClient({ host, email: email3, apiToken }), host };
|
|
17348
|
+
}
|
|
17349
|
+
|
|
17350
|
+
// src/skills/builtin/jira/tools.ts
|
|
17351
|
+
var JIRA_TYPE = "jira-issue";
|
|
17352
|
+
function jiraNotConfiguredError() {
|
|
17353
|
+
return {
|
|
17354
|
+
content: [
|
|
17355
|
+
{
|
|
17356
|
+
type: "text",
|
|
17357
|
+
text: 'Jira is not configured. Run "marvin config jira" or set JIRA_HOST, JIRA_EMAIL, and JIRA_API_TOKEN environment variables.'
|
|
17358
|
+
}
|
|
17359
|
+
],
|
|
17360
|
+
isError: true
|
|
17361
|
+
};
|
|
17362
|
+
}
|
|
17363
|
+
function mapJiraStatus(jiraStatus) {
|
|
17364
|
+
const lower = jiraStatus.toLowerCase();
|
|
17365
|
+
if (lower === "done" || lower === "closed" || lower === "resolved") return "done";
|
|
17366
|
+
if (lower === "in progress" || lower === "in review") return "in-progress";
|
|
17367
|
+
return "open";
|
|
17368
|
+
}
|
|
17369
|
+
function jiraIssueToFrontmatter(issue2, host, linkedArtifacts) {
|
|
17370
|
+
return {
|
|
17371
|
+
title: issue2.fields.summary,
|
|
17372
|
+
status: mapJiraStatus(issue2.fields.status.name),
|
|
17373
|
+
jiraKey: issue2.key,
|
|
17374
|
+
jiraUrl: `https://${host}/browse/${issue2.key}`,
|
|
17375
|
+
issueType: issue2.fields.issuetype.name,
|
|
17376
|
+
priority: issue2.fields.priority?.name ?? "None",
|
|
17377
|
+
assignee: issue2.fields.assignee?.displayName ?? "",
|
|
17378
|
+
labels: issue2.fields.labels ?? [],
|
|
17379
|
+
linkedArtifacts: linkedArtifacts ?? [],
|
|
17380
|
+
tags: [`jira:${issue2.key}`],
|
|
17381
|
+
lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
17382
|
+
};
|
|
17383
|
+
}
|
|
17384
|
+
function findByJiraKey(store, jiraKey) {
|
|
17385
|
+
const docs = store.list({ type: JIRA_TYPE });
|
|
17386
|
+
return docs.find((d) => d.frontmatter.jiraKey === jiraKey);
|
|
17387
|
+
}
|
|
17388
|
+
function createJiraTools(store) {
|
|
17389
|
+
const jiraUserConfig = loadUserConfig().jira;
|
|
17390
|
+
return [
|
|
17391
|
+
// --- Local read tools ---
|
|
17392
|
+
tool19(
|
|
17393
|
+
"list_jira_issues",
|
|
17394
|
+
"List locally synced Jira issues (JI-xxx documents), optionally filtered by status or Jira key",
|
|
17395
|
+
{
|
|
17396
|
+
status: external_exports.enum(["open", "in-progress", "done"]).optional().describe("Filter by local status"),
|
|
17397
|
+
jiraKey: external_exports.string().optional().describe("Filter by Jira issue key (e.g. 'PROJ-123')")
|
|
17398
|
+
},
|
|
17399
|
+
async (args) => {
|
|
17400
|
+
let docs = store.list({ type: JIRA_TYPE, status: args.status });
|
|
17401
|
+
if (args.jiraKey) {
|
|
17402
|
+
docs = docs.filter((d) => d.frontmatter.jiraKey === args.jiraKey);
|
|
17403
|
+
}
|
|
17404
|
+
const summary = docs.map((d) => ({
|
|
17405
|
+
id: d.frontmatter.id,
|
|
17406
|
+
title: d.frontmatter.title,
|
|
17407
|
+
status: d.frontmatter.status,
|
|
17408
|
+
jiraKey: d.frontmatter.jiraKey,
|
|
17409
|
+
issueType: d.frontmatter.issueType,
|
|
17410
|
+
priority: d.frontmatter.priority,
|
|
17411
|
+
assignee: d.frontmatter.assignee,
|
|
17412
|
+
linkedArtifacts: d.frontmatter.linkedArtifacts
|
|
17413
|
+
}));
|
|
17414
|
+
return {
|
|
17415
|
+
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
17416
|
+
};
|
|
17417
|
+
},
|
|
17418
|
+
{ annotations: { readOnly: true } }
|
|
17419
|
+
),
|
|
17420
|
+
tool19(
|
|
17421
|
+
"get_jira_issue",
|
|
17422
|
+
"Get the full content of a locally synced Jira issue by local ID (JI-xxx) or Jira key (PROJ-123)",
|
|
17423
|
+
{
|
|
17424
|
+
id: external_exports.string().describe("Local ID (e.g. 'JI-001') or Jira key (e.g. 'PROJ-123')")
|
|
17425
|
+
},
|
|
17426
|
+
async (args) => {
|
|
17427
|
+
let doc = store.get(args.id);
|
|
17428
|
+
if (!doc) {
|
|
17429
|
+
doc = findByJiraKey(store, args.id);
|
|
17430
|
+
}
|
|
17431
|
+
if (!doc) {
|
|
17432
|
+
return {
|
|
17433
|
+
content: [{ type: "text", text: `Jira issue ${args.id} not found locally` }],
|
|
17434
|
+
isError: true
|
|
17435
|
+
};
|
|
17436
|
+
}
|
|
17437
|
+
return {
|
|
17438
|
+
content: [
|
|
17439
|
+
{
|
|
17440
|
+
type: "text",
|
|
17441
|
+
text: JSON.stringify(
|
|
17442
|
+
{ ...doc.frontmatter, content: doc.content },
|
|
17443
|
+
null,
|
|
17444
|
+
2
|
|
17445
|
+
)
|
|
17446
|
+
}
|
|
17447
|
+
]
|
|
17448
|
+
};
|
|
17449
|
+
},
|
|
17450
|
+
{ annotations: { readOnly: true } }
|
|
17451
|
+
),
|
|
17452
|
+
// --- Jira → Local tools ---
|
|
17453
|
+
tool19(
|
|
17454
|
+
"pull_jira_issue",
|
|
17455
|
+
"Fetch a single Jira issue by key and create/update a local JI-xxx document",
|
|
17456
|
+
{
|
|
17457
|
+
key: external_exports.string().describe("Jira issue key (e.g. 'PROJ-123')")
|
|
17458
|
+
},
|
|
17459
|
+
async (args) => {
|
|
17460
|
+
const jira = createJiraClient(jiraUserConfig);
|
|
17461
|
+
if (!jira) return jiraNotConfiguredError();
|
|
17462
|
+
const issue2 = await jira.client.getIssue(args.key);
|
|
17463
|
+
const existing = findByJiraKey(store, args.key);
|
|
17464
|
+
if (existing) {
|
|
17465
|
+
const fm2 = jiraIssueToFrontmatter(
|
|
17466
|
+
issue2,
|
|
17467
|
+
jira.host,
|
|
17468
|
+
existing.frontmatter.linkedArtifacts
|
|
17469
|
+
);
|
|
17470
|
+
const doc2 = store.update(
|
|
17471
|
+
existing.frontmatter.id,
|
|
17472
|
+
fm2,
|
|
17473
|
+
issue2.fields.description ?? ""
|
|
17474
|
+
);
|
|
17475
|
+
return {
|
|
17476
|
+
content: [
|
|
17477
|
+
{
|
|
17478
|
+
type: "text",
|
|
17479
|
+
text: `Updated ${doc2.frontmatter.id} from Jira ${args.key}`
|
|
17480
|
+
}
|
|
17481
|
+
]
|
|
17482
|
+
};
|
|
17483
|
+
}
|
|
17484
|
+
const fm = jiraIssueToFrontmatter(issue2, jira.host);
|
|
17485
|
+
const doc = store.create(
|
|
17486
|
+
JIRA_TYPE,
|
|
17487
|
+
fm,
|
|
17488
|
+
issue2.fields.description ?? ""
|
|
17489
|
+
);
|
|
17490
|
+
return {
|
|
17491
|
+
content: [
|
|
17492
|
+
{
|
|
17493
|
+
type: "text",
|
|
17494
|
+
text: `Created ${doc.frontmatter.id} from Jira ${args.key}`
|
|
17495
|
+
}
|
|
17496
|
+
]
|
|
17497
|
+
};
|
|
17498
|
+
}
|
|
17499
|
+
),
|
|
17500
|
+
tool19(
|
|
17501
|
+
"pull_jira_issues_jql",
|
|
17502
|
+
"Bulk fetch Jira issues via JQL query and create/update local JI-xxx documents",
|
|
17503
|
+
{
|
|
17504
|
+
jql: external_exports.string().describe(`JQL query (e.g. 'project = PROJ AND status = "In Progress"')`),
|
|
17505
|
+
maxResults: external_exports.number().optional().describe("Max issues to fetch (default 50)")
|
|
17506
|
+
},
|
|
17507
|
+
async (args) => {
|
|
17508
|
+
const jira = createJiraClient(jiraUserConfig);
|
|
17509
|
+
if (!jira) return jiraNotConfiguredError();
|
|
17510
|
+
const result = await jira.client.searchIssues(args.jql, args.maxResults);
|
|
17511
|
+
const created = [];
|
|
17512
|
+
const updated = [];
|
|
17513
|
+
for (const issue2 of result.issues) {
|
|
17514
|
+
const existing = findByJiraKey(store, issue2.key);
|
|
17515
|
+
if (existing) {
|
|
17516
|
+
const fm = jiraIssueToFrontmatter(
|
|
17517
|
+
issue2,
|
|
17518
|
+
jira.host,
|
|
17519
|
+
existing.frontmatter.linkedArtifacts
|
|
17520
|
+
);
|
|
17521
|
+
store.update(
|
|
17522
|
+
existing.frontmatter.id,
|
|
17523
|
+
fm,
|
|
17524
|
+
issue2.fields.description ?? ""
|
|
17525
|
+
);
|
|
17526
|
+
updated.push(`${existing.frontmatter.id} (${issue2.key})`);
|
|
17527
|
+
} else {
|
|
17528
|
+
const fm = jiraIssueToFrontmatter(issue2, jira.host);
|
|
17529
|
+
const doc = store.create(
|
|
17530
|
+
JIRA_TYPE,
|
|
17531
|
+
fm,
|
|
17532
|
+
issue2.fields.description ?? ""
|
|
17533
|
+
);
|
|
17534
|
+
created.push(`${doc.frontmatter.id} (${issue2.key})`);
|
|
17535
|
+
}
|
|
17536
|
+
}
|
|
17537
|
+
const parts = [
|
|
17538
|
+
`Fetched ${result.issues.length} of ${result.total} matching issues.`
|
|
17539
|
+
];
|
|
17540
|
+
if (created.length > 0) parts.push(`Created: ${created.join(", ")}`);
|
|
17541
|
+
if (updated.length > 0) parts.push(`Updated: ${updated.join(", ")}`);
|
|
17542
|
+
return {
|
|
17543
|
+
content: [{ type: "text", text: parts.join("\n") }]
|
|
17544
|
+
};
|
|
17545
|
+
}
|
|
17546
|
+
),
|
|
17547
|
+
// --- Local → Jira tools ---
|
|
17548
|
+
tool19(
|
|
17549
|
+
"push_artifact_to_jira",
|
|
17550
|
+
"Create a Jira issue from any Marvin artifact (D/A/Q/F/E) and create a tracking JI-xxx document",
|
|
17551
|
+
{
|
|
17552
|
+
artifactId: external_exports.string().describe("Marvin artifact ID (e.g. 'D-001', 'F-003', 'E-002')"),
|
|
17553
|
+
projectKey: external_exports.string().describe("Jira project key (e.g. 'PROJ')"),
|
|
17554
|
+
issueType: external_exports.enum(["Story", "Task", "Bug", "Epic"]).optional().describe("Jira issue type (default: 'Task')")
|
|
17555
|
+
},
|
|
17556
|
+
async (args) => {
|
|
17557
|
+
const jira = createJiraClient(jiraUserConfig);
|
|
17558
|
+
if (!jira) return jiraNotConfiguredError();
|
|
17559
|
+
const artifact = store.get(args.artifactId);
|
|
17560
|
+
if (!artifact) {
|
|
17561
|
+
return {
|
|
17562
|
+
content: [
|
|
17563
|
+
{ type: "text", text: `Artifact ${args.artifactId} not found` }
|
|
17564
|
+
],
|
|
17565
|
+
isError: true
|
|
17566
|
+
};
|
|
17567
|
+
}
|
|
17568
|
+
const description = [
|
|
17569
|
+
artifact.content,
|
|
17570
|
+
"",
|
|
17571
|
+
`---`,
|
|
17572
|
+
`Marvin artifact: ${artifact.frontmatter.id} (${artifact.frontmatter.type})`,
|
|
17573
|
+
`Status: ${artifact.frontmatter.status}`
|
|
17574
|
+
].join("\n");
|
|
17575
|
+
const jiraResult = await jira.client.createIssue({
|
|
17576
|
+
project: { key: args.projectKey },
|
|
17577
|
+
summary: artifact.frontmatter.title,
|
|
17578
|
+
description,
|
|
17579
|
+
issuetype: { name: args.issueType ?? "Task" }
|
|
17580
|
+
});
|
|
17581
|
+
const jiDoc = store.create(
|
|
17582
|
+
JIRA_TYPE,
|
|
17583
|
+
{
|
|
17584
|
+
title: artifact.frontmatter.title,
|
|
17585
|
+
status: "open",
|
|
17586
|
+
jiraKey: jiraResult.key,
|
|
17587
|
+
jiraUrl: `https://${jira.host}/browse/${jiraResult.key}`,
|
|
17588
|
+
issueType: args.issueType ?? "Task",
|
|
17589
|
+
priority: "Medium",
|
|
17590
|
+
assignee: "",
|
|
17591
|
+
labels: [],
|
|
17592
|
+
linkedArtifacts: [args.artifactId],
|
|
17593
|
+
tags: [`jira:${jiraResult.key}`],
|
|
17594
|
+
lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
17595
|
+
},
|
|
17596
|
+
""
|
|
17597
|
+
);
|
|
17598
|
+
return {
|
|
17599
|
+
content: [
|
|
17600
|
+
{
|
|
17601
|
+
type: "text",
|
|
17602
|
+
text: `Created Jira ${jiraResult.key} from ${args.artifactId}. Tracking locally as ${jiDoc.frontmatter.id}.`
|
|
17603
|
+
}
|
|
17604
|
+
]
|
|
17605
|
+
};
|
|
17606
|
+
}
|
|
17607
|
+
),
|
|
17608
|
+
// --- Bidirectional sync ---
|
|
17609
|
+
tool19(
|
|
17610
|
+
"sync_jira_issue",
|
|
17611
|
+
"Bidirectional sync: push local title/description to Jira, pull latest status/assignee/labels back",
|
|
17612
|
+
{
|
|
17613
|
+
id: external_exports.string().describe("Local JI-xxx ID")
|
|
17614
|
+
},
|
|
17615
|
+
async (args) => {
|
|
17616
|
+
const jira = createJiraClient(jiraUserConfig);
|
|
17617
|
+
if (!jira) return jiraNotConfiguredError();
|
|
17618
|
+
const doc = store.get(args.id);
|
|
17619
|
+
if (!doc || doc.frontmatter.type !== JIRA_TYPE) {
|
|
17620
|
+
return {
|
|
17621
|
+
content: [
|
|
17622
|
+
{ type: "text", text: `Jira issue ${args.id} not found locally` }
|
|
17623
|
+
],
|
|
17624
|
+
isError: true
|
|
17625
|
+
};
|
|
17626
|
+
}
|
|
17627
|
+
const jiraKey = doc.frontmatter.jiraKey;
|
|
17628
|
+
await jira.client.updateIssue(jiraKey, {
|
|
17629
|
+
summary: doc.frontmatter.title,
|
|
17630
|
+
description: doc.content || void 0
|
|
17631
|
+
});
|
|
17632
|
+
const issue2 = await jira.client.getIssue(jiraKey);
|
|
17633
|
+
const fm = jiraIssueToFrontmatter(
|
|
17634
|
+
issue2,
|
|
17635
|
+
jira.host,
|
|
17636
|
+
doc.frontmatter.linkedArtifacts
|
|
17637
|
+
);
|
|
17638
|
+
store.update(args.id, fm, issue2.fields.description ?? "");
|
|
17639
|
+
return {
|
|
17640
|
+
content: [
|
|
17641
|
+
{
|
|
17642
|
+
type: "text",
|
|
17643
|
+
text: `Synced ${args.id} \u2194 ${jiraKey}. Status: ${fm.status}, Assignee: ${fm.assignee || "unassigned"}`
|
|
17644
|
+
}
|
|
17645
|
+
]
|
|
17646
|
+
};
|
|
17647
|
+
}
|
|
17648
|
+
),
|
|
17649
|
+
// --- Local link tool ---
|
|
17650
|
+
tool19(
|
|
17651
|
+
"link_artifact_to_jira",
|
|
17652
|
+
"Add a Marvin artifact ID to a JI-xxx document's linkedArtifacts field",
|
|
17653
|
+
{
|
|
17654
|
+
jiraIssueId: external_exports.string().describe("Local JI-xxx ID"),
|
|
17655
|
+
artifactId: external_exports.string().describe("Marvin artifact ID to link (e.g. 'D-001', 'F-003')")
|
|
17656
|
+
},
|
|
17657
|
+
async (args) => {
|
|
17658
|
+
const doc = store.get(args.jiraIssueId);
|
|
17659
|
+
if (!doc || doc.frontmatter.type !== JIRA_TYPE) {
|
|
17660
|
+
return {
|
|
17661
|
+
content: [
|
|
17662
|
+
{
|
|
17663
|
+
type: "text",
|
|
17664
|
+
text: `Jira issue ${args.jiraIssueId} not found locally`
|
|
17665
|
+
}
|
|
17666
|
+
],
|
|
17667
|
+
isError: true
|
|
17668
|
+
};
|
|
17669
|
+
}
|
|
17670
|
+
const artifact = store.get(args.artifactId);
|
|
17671
|
+
if (!artifact) {
|
|
17672
|
+
return {
|
|
17673
|
+
content: [
|
|
17674
|
+
{ type: "text", text: `Artifact ${args.artifactId} not found` }
|
|
17675
|
+
],
|
|
17676
|
+
isError: true
|
|
17677
|
+
};
|
|
17678
|
+
}
|
|
17679
|
+
const linked = doc.frontmatter.linkedArtifacts ?? [];
|
|
17680
|
+
if (linked.includes(args.artifactId)) {
|
|
17681
|
+
return {
|
|
17682
|
+
content: [
|
|
17683
|
+
{
|
|
17684
|
+
type: "text",
|
|
17685
|
+
text: `${args.artifactId} is already linked to ${args.jiraIssueId}`
|
|
17686
|
+
}
|
|
17687
|
+
]
|
|
17688
|
+
};
|
|
17689
|
+
}
|
|
17690
|
+
store.update(args.jiraIssueId, {
|
|
17691
|
+
linkedArtifacts: [...linked, args.artifactId]
|
|
17692
|
+
});
|
|
17693
|
+
return {
|
|
17694
|
+
content: [
|
|
17695
|
+
{
|
|
17696
|
+
type: "text",
|
|
17697
|
+
text: `Linked ${args.artifactId} to ${args.jiraIssueId}`
|
|
17698
|
+
}
|
|
17699
|
+
]
|
|
17700
|
+
};
|
|
17701
|
+
}
|
|
17702
|
+
)
|
|
17703
|
+
];
|
|
17704
|
+
}
|
|
17705
|
+
|
|
17706
|
+
// src/skills/builtin/jira/index.ts
|
|
17707
|
+
var jiraSkill = {
|
|
17708
|
+
id: "jira",
|
|
17709
|
+
name: "Jira Integration",
|
|
17710
|
+
description: "Bidirectional sync between Marvin artifacts and Jira issues",
|
|
17711
|
+
version: "1.0.0",
|
|
17712
|
+
format: "builtin-ts",
|
|
17713
|
+
// No default persona affinity — opt-in via config.yaml skills section
|
|
17714
|
+
documentTypeRegistrations: [
|
|
17715
|
+
{ type: "jira-issue", dirName: "jira-issues", idPrefix: "JI" }
|
|
17716
|
+
],
|
|
17717
|
+
tools: (store) => createJiraTools(store),
|
|
17718
|
+
promptFragments: {
|
|
17719
|
+
"product-owner": `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, feature, 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 Product Owner, use Jira integration to:**
|
|
17729
|
+
- Pull stakeholder-reported issues for triage and prioritization
|
|
17730
|
+
- Push approved features as Stories for development tracking
|
|
17731
|
+
- Link decisions to Jira issues for audit trail and traceability
|
|
17732
|
+
- Use JQL queries to review backlog status (e.g. \`project = PROJ AND status = "To Do"\`)`,
|
|
17733
|
+
"tech-lead": `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, epic, 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 Tech Lead, use Jira integration to:**
|
|
17743
|
+
- Pull technical issues and bugs for sprint planning and estimation
|
|
17744
|
+
- Push epics and technical decisions to Jira for cross-team visibility
|
|
17745
|
+
- Bidirectional sync to keep local governance and Jira in alignment
|
|
17746
|
+
- Use JQL queries to track technical debt (e.g. \`labels = "tech-debt" AND status != "Done"\`)`,
|
|
17747
|
+
"delivery-manager": `You have the **Jira Integration** skill. You can pull issues from Jira and push Marvin artifacts to Jira.
|
|
17748
|
+
|
|
17749
|
+
**Available tools:**
|
|
17750
|
+
- \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues
|
|
17751
|
+
- \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import issues from Jira by key or JQL query
|
|
17752
|
+
- \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, action, etc.)
|
|
17753
|
+
- \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
|
|
17754
|
+
- \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx
|
|
17755
|
+
|
|
17756
|
+
**As Delivery Manager, use Jira integration to:**
|
|
17757
|
+
- Pull sprint issues for tracking progress and blockers
|
|
17758
|
+
- Push actions and decisions to Jira for stakeholder visibility
|
|
17759
|
+
- Use JQL queries for reporting (e.g. \`sprint in openSprints() AND assignee = currentUser()\`)
|
|
17760
|
+
- Sync status between Marvin governance items and Jira issues`
|
|
17761
|
+
}
|
|
17762
|
+
};
|
|
17763
|
+
|
|
17174
17764
|
// src/skills/registry.ts
|
|
17175
17765
|
var BUILTIN_SKILLS = {
|
|
17176
|
-
"governance-review": governanceReviewSkill
|
|
17766
|
+
"governance-review": governanceReviewSkill,
|
|
17767
|
+
"jira": jiraSkill
|
|
17177
17768
|
};
|
|
17178
17769
|
function getBuiltinSkillsDir() {
|
|
17179
17770
|
const thisFile = fileURLToPath(import.meta.url);
|
|
@@ -17330,7 +17921,7 @@ ${fragment}`);
|
|
|
17330
17921
|
}
|
|
17331
17922
|
|
|
17332
17923
|
// src/skills/action-tools.ts
|
|
17333
|
-
import { tool as
|
|
17924
|
+
import { tool as tool20 } from "@anthropic-ai/claude-agent-sdk";
|
|
17334
17925
|
|
|
17335
17926
|
// src/skills/action-runner.ts
|
|
17336
17927
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
@@ -17420,7 +18011,7 @@ function createSkillActionTools(skills, context) {
|
|
|
17420
18011
|
if (!skill.actions) continue;
|
|
17421
18012
|
for (const action of skill.actions) {
|
|
17422
18013
|
tools.push(
|
|
17423
|
-
|
|
18014
|
+
tool20(
|
|
17424
18015
|
`${skill.id}__${action.id}`,
|
|
17425
18016
|
action.description,
|
|
17426
18017
|
{
|
|
@@ -17647,10 +18238,10 @@ ${lines.join("\n\n")}`;
|
|
|
17647
18238
|
}
|
|
17648
18239
|
|
|
17649
18240
|
// src/mcp/persona-tools.ts
|
|
17650
|
-
import { tool as
|
|
18241
|
+
import { tool as tool21 } from "@anthropic-ai/claude-agent-sdk";
|
|
17651
18242
|
function createPersonaTools(ctx, marvinDir) {
|
|
17652
18243
|
return [
|
|
17653
|
-
|
|
18244
|
+
tool21(
|
|
17654
18245
|
"set_persona",
|
|
17655
18246
|
"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.",
|
|
17656
18247
|
{
|
|
@@ -17680,7 +18271,7 @@ ${summaries}`
|
|
|
17680
18271
|
};
|
|
17681
18272
|
}
|
|
17682
18273
|
),
|
|
17683
|
-
|
|
18274
|
+
tool21(
|
|
17684
18275
|
"get_persona_guidance",
|
|
17685
18276
|
"Get guidance for a persona without changing the active persona. If no persona is specified, lists all available personas with summaries.",
|
|
17686
18277
|
{
|