mrvn-cli 0.2.6 → 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.
@@ -14971,6 +14971,128 @@ function createMeetingTools(store) {
14971
14971
 
14972
14972
  // src/plugins/builtin/tools/reports.ts
14973
14973
  import { tool as tool8 } from "@anthropic-ai/claude-agent-sdk";
14974
+
14975
+ // src/reports/gar/collector.ts
14976
+ function collectGarMetrics(store) {
14977
+ const allActions = store.list({ type: "action" });
14978
+ const openActions = allActions.filter((d) => d.frontmatter.status === "open");
14979
+ const doneActions = allActions.filter((d) => d.frontmatter.status === "done");
14980
+ const allDocs = store.list();
14981
+ const blockedItems = allDocs.filter(
14982
+ (d) => d.frontmatter.tags?.includes("blocked")
14983
+ );
14984
+ const overdueItems = allDocs.filter(
14985
+ (d) => d.frontmatter.tags?.includes("overdue")
14986
+ );
14987
+ const openQuestions = store.list({ type: "question", status: "open" });
14988
+ const riskItems = allDocs.filter(
14989
+ (d) => d.frontmatter.tags?.includes("risk")
14990
+ );
14991
+ const unownedActions = openActions.filter((d) => !d.frontmatter.owner);
14992
+ const total = allActions.length;
14993
+ const done = doneActions.length;
14994
+ const completionPct = total > 0 ? Math.round(done / total * 100) : 100;
14995
+ const scheduleItems = [
14996
+ ...blockedItems,
14997
+ ...overdueItems
14998
+ ].filter(
14999
+ (d, i, arr) => arr.findIndex((x) => x.frontmatter.id === d.frontmatter.id) === i
15000
+ ).map((d) => ({ id: d.frontmatter.id, title: d.frontmatter.title }));
15001
+ const qualityItems = [
15002
+ ...riskItems,
15003
+ ...openQuestions
15004
+ ].filter(
15005
+ (d, i, arr) => arr.findIndex((x) => x.frontmatter.id === d.frontmatter.id) === i
15006
+ ).map((d) => ({ id: d.frontmatter.id, title: d.frontmatter.title }));
15007
+ const resourceItems = unownedActions.map((d) => ({
15008
+ id: d.frontmatter.id,
15009
+ title: d.frontmatter.title
15010
+ }));
15011
+ return {
15012
+ scope: {
15013
+ total,
15014
+ open: openActions.length,
15015
+ done,
15016
+ completionPct
15017
+ },
15018
+ schedule: {
15019
+ blocked: blockedItems.length,
15020
+ overdue: overdueItems.length,
15021
+ items: scheduleItems
15022
+ },
15023
+ quality: {
15024
+ risks: riskItems.length,
15025
+ openQuestions: openQuestions.length,
15026
+ items: qualityItems
15027
+ },
15028
+ resources: {
15029
+ unowned: unownedActions.length,
15030
+ items: resourceItems
15031
+ }
15032
+ };
15033
+ }
15034
+
15035
+ // src/reports/gar/evaluator.ts
15036
+ function worstStatus(statuses) {
15037
+ if (statuses.includes("red")) return "red";
15038
+ if (statuses.includes("amber")) return "amber";
15039
+ return "green";
15040
+ }
15041
+ function evaluateGar(projectName, metrics) {
15042
+ const areas = [];
15043
+ const scopePct = metrics.scope.completionPct;
15044
+ const scopeStatus = scopePct >= 70 ? "green" : scopePct >= 40 ? "amber" : "red";
15045
+ areas.push({
15046
+ name: "Scope",
15047
+ status: scopeStatus,
15048
+ summary: `${scopePct}% complete (${metrics.scope.done}/${metrics.scope.total})`,
15049
+ items: []
15050
+ });
15051
+ const scheduleCount = metrics.schedule.blocked + metrics.schedule.overdue;
15052
+ const scheduleStatus = scheduleCount === 0 ? "green" : scheduleCount <= 2 ? "amber" : "red";
15053
+ const scheduleParts = [];
15054
+ if (metrics.schedule.blocked > 0)
15055
+ scheduleParts.push(`${metrics.schedule.blocked} blocked`);
15056
+ if (metrics.schedule.overdue > 0)
15057
+ scheduleParts.push(`${metrics.schedule.overdue} overdue`);
15058
+ areas.push({
15059
+ name: "Schedule",
15060
+ status: scheduleStatus,
15061
+ summary: scheduleParts.length > 0 ? scheduleParts.join(", ") : "on track",
15062
+ items: metrics.schedule.items
15063
+ });
15064
+ const qualityCount = metrics.quality.risks + metrics.quality.openQuestions;
15065
+ const qualityStatus = qualityCount === 0 ? "green" : qualityCount <= 2 ? "amber" : "red";
15066
+ const qualityParts = [];
15067
+ if (metrics.quality.risks > 0)
15068
+ qualityParts.push(`${metrics.quality.risks} risk(s)`);
15069
+ if (metrics.quality.openQuestions > 0)
15070
+ qualityParts.push(`${metrics.quality.openQuestions} open question(s)`);
15071
+ areas.push({
15072
+ name: "Quality",
15073
+ status: qualityStatus,
15074
+ summary: qualityParts.length > 0 ? qualityParts.join(", ") : "no issues",
15075
+ items: metrics.quality.items
15076
+ });
15077
+ const resourceCount = metrics.resources.unowned;
15078
+ const resourceStatus = resourceCount === 0 ? "green" : resourceCount <= 2 ? "amber" : "red";
15079
+ areas.push({
15080
+ name: "Resources",
15081
+ status: resourceStatus,
15082
+ summary: resourceCount > 0 ? `${resourceCount} unowned action(s)` : "all assigned",
15083
+ items: metrics.resources.items
15084
+ });
15085
+ const overall = worstStatus(areas.map((a) => a.status));
15086
+ return {
15087
+ projectName,
15088
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
15089
+ overall,
15090
+ areas,
15091
+ metrics
15092
+ };
15093
+ }
15094
+
15095
+ // src/plugins/builtin/tools/reports.ts
14974
15096
  function createReportTools(store) {
14975
15097
  return [
14976
15098
  tool8(
@@ -15059,41 +15181,10 @@ function createReportTools(store) {
15059
15181
  "Generate a Green-Amber-Red report with metrics across scope, schedule, quality, and resources",
15060
15182
  {},
15061
15183
  async () => {
15062
- const allActions = store.list({ type: "action" });
15063
- const openActions = allActions.filter((d) => d.frontmatter.status === "open");
15064
- const doneActions = allActions.filter((d) => d.frontmatter.status === "done");
15065
- const allDocs = store.list();
15066
- const blockedItems = allDocs.filter(
15067
- (d) => d.frontmatter.tags?.includes("blocked")
15068
- );
15069
- const overdueItems = allDocs.filter(
15070
- (d) => d.frontmatter.tags?.includes("overdue")
15071
- );
15072
- const openQuestions = store.list({ type: "question", status: "open" });
15073
- const riskItems = allDocs.filter(
15074
- (d) => d.frontmatter.tags?.includes("risk")
15075
- );
15076
- const unownedActions = openActions.filter((d) => !d.frontmatter.owner);
15077
- const areas = {
15078
- scope: {
15079
- total: allActions.length,
15080
- open: openActions.length,
15081
- done: doneActions.length
15082
- },
15083
- schedule: {
15084
- blocked: blockedItems.length,
15085
- overdue: overdueItems.length
15086
- },
15087
- quality: {
15088
- openQuestions: openQuestions.length,
15089
- risks: riskItems.length
15090
- },
15091
- resources: {
15092
- unowned: unownedActions.length
15093
- }
15094
- };
15184
+ const metrics = collectGarMetrics(store);
15185
+ const report = evaluateGar("project", metrics);
15095
15186
  return {
15096
- content: [{ type: "text", text: JSON.stringify({ areas }, null, 2) }]
15187
+ content: [{ type: "text", text: JSON.stringify(report, null, 2) }]
15097
15188
  };
15098
15189
  },
15099
15190
  { annotations: { readOnly: true } }
@@ -17171,9 +17262,495 @@ Be thorough but concise. Focus on actionable insights.`,
17171
17262
  ]
17172
17263
  };
17173
17264
 
17265
+ // src/skills/builtin/jira/tools.ts
17266
+ import { tool as tool19 } from "@anthropic-ai/claude-agent-sdk";
17267
+
17268
+ // src/skills/builtin/jira/client.ts
17269
+ var JiraClient = class {
17270
+ baseUrl;
17271
+ authHeader;
17272
+ constructor(config2) {
17273
+ this.baseUrl = `https://${config2.host}/rest/api/2`;
17274
+ this.authHeader = "Basic " + Buffer.from(`${config2.email}:${config2.apiToken}`).toString("base64");
17275
+ }
17276
+ async request(path10, method = "GET", body) {
17277
+ const url2 = `${this.baseUrl}${path10}`;
17278
+ const headers = {
17279
+ Authorization: this.authHeader,
17280
+ "Content-Type": "application/json",
17281
+ Accept: "application/json"
17282
+ };
17283
+ const response = await fetch(url2, {
17284
+ method,
17285
+ headers,
17286
+ body: body ? JSON.stringify(body) : void 0
17287
+ });
17288
+ if (!response.ok) {
17289
+ const text = await response.text().catch(() => "");
17290
+ throw new Error(
17291
+ `Jira API error ${response.status} ${method} ${path10}: ${text}`
17292
+ );
17293
+ }
17294
+ if (response.status === 204) return void 0;
17295
+ return response.json();
17296
+ }
17297
+ async searchIssues(jql, maxResults = 50) {
17298
+ const params = new URLSearchParams({
17299
+ jql,
17300
+ maxResults: String(maxResults)
17301
+ });
17302
+ return this.request(`/search?${params}`);
17303
+ }
17304
+ async getIssue(key) {
17305
+ return this.request(`/issue/${encodeURIComponent(key)}`);
17306
+ }
17307
+ async createIssue(fields) {
17308
+ return this.request("/issue", "POST", { fields });
17309
+ }
17310
+ async updateIssue(key, fields) {
17311
+ await this.request(
17312
+ `/issue/${encodeURIComponent(key)}`,
17313
+ "PUT",
17314
+ { fields }
17315
+ );
17316
+ }
17317
+ async addComment(key, body) {
17318
+ await this.request(
17319
+ `/issue/${encodeURIComponent(key)}/comment`,
17320
+ "POST",
17321
+ { body }
17322
+ );
17323
+ }
17324
+ };
17325
+ function createJiraClient() {
17326
+ const host = process.env.JIRA_HOST;
17327
+ const email3 = process.env.JIRA_EMAIL;
17328
+ const apiToken = process.env.JIRA_API_TOKEN;
17329
+ if (!host || !email3 || !apiToken) return null;
17330
+ return new JiraClient({ host, email: email3, apiToken });
17331
+ }
17332
+
17333
+ // src/skills/builtin/jira/tools.ts
17334
+ var JIRA_TYPE = "jira-issue";
17335
+ function jiraNotConfiguredError() {
17336
+ return {
17337
+ content: [
17338
+ {
17339
+ type: "text",
17340
+ text: "Jira is not configured. Set JIRA_HOST, JIRA_EMAIL, and JIRA_API_TOKEN environment variables."
17341
+ }
17342
+ ],
17343
+ isError: true
17344
+ };
17345
+ }
17346
+ function mapJiraStatus(jiraStatus) {
17347
+ const lower = jiraStatus.toLowerCase();
17348
+ if (lower === "done" || lower === "closed" || lower === "resolved") return "done";
17349
+ if (lower === "in progress" || lower === "in review") return "in-progress";
17350
+ return "open";
17351
+ }
17352
+ function jiraIssueToFrontmatter(issue2, host, linkedArtifacts) {
17353
+ return {
17354
+ title: issue2.fields.summary,
17355
+ status: mapJiraStatus(issue2.fields.status.name),
17356
+ jiraKey: issue2.key,
17357
+ jiraUrl: `https://${host}/browse/${issue2.key}`,
17358
+ issueType: issue2.fields.issuetype.name,
17359
+ priority: issue2.fields.priority?.name ?? "None",
17360
+ assignee: issue2.fields.assignee?.displayName ?? "",
17361
+ labels: issue2.fields.labels ?? [],
17362
+ linkedArtifacts: linkedArtifacts ?? [],
17363
+ tags: [`jira:${issue2.key}`],
17364
+ lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString()
17365
+ };
17366
+ }
17367
+ function findByJiraKey(store, jiraKey) {
17368
+ const docs = store.list({ type: JIRA_TYPE });
17369
+ return docs.find((d) => d.frontmatter.jiraKey === jiraKey);
17370
+ }
17371
+ function createJiraTools(store) {
17372
+ return [
17373
+ // --- Local read tools ---
17374
+ tool19(
17375
+ "list_jira_issues",
17376
+ "List locally synced Jira issues (JI-xxx documents), optionally filtered by status or Jira key",
17377
+ {
17378
+ status: external_exports.enum(["open", "in-progress", "done"]).optional().describe("Filter by local status"),
17379
+ jiraKey: external_exports.string().optional().describe("Filter by Jira issue key (e.g. 'PROJ-123')")
17380
+ },
17381
+ async (args) => {
17382
+ let docs = store.list({ type: JIRA_TYPE, status: args.status });
17383
+ if (args.jiraKey) {
17384
+ docs = docs.filter((d) => d.frontmatter.jiraKey === args.jiraKey);
17385
+ }
17386
+ const summary = docs.map((d) => ({
17387
+ id: d.frontmatter.id,
17388
+ title: d.frontmatter.title,
17389
+ status: d.frontmatter.status,
17390
+ jiraKey: d.frontmatter.jiraKey,
17391
+ issueType: d.frontmatter.issueType,
17392
+ priority: d.frontmatter.priority,
17393
+ assignee: d.frontmatter.assignee,
17394
+ linkedArtifacts: d.frontmatter.linkedArtifacts
17395
+ }));
17396
+ return {
17397
+ content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
17398
+ };
17399
+ },
17400
+ { annotations: { readOnly: true } }
17401
+ ),
17402
+ tool19(
17403
+ "get_jira_issue",
17404
+ "Get the full content of a locally synced Jira issue by local ID (JI-xxx) or Jira key (PROJ-123)",
17405
+ {
17406
+ id: external_exports.string().describe("Local ID (e.g. 'JI-001') or Jira key (e.g. 'PROJ-123')")
17407
+ },
17408
+ async (args) => {
17409
+ let doc = store.get(args.id);
17410
+ if (!doc) {
17411
+ doc = findByJiraKey(store, args.id);
17412
+ }
17413
+ if (!doc) {
17414
+ return {
17415
+ content: [{ type: "text", text: `Jira issue ${args.id} not found locally` }],
17416
+ isError: true
17417
+ };
17418
+ }
17419
+ return {
17420
+ content: [
17421
+ {
17422
+ type: "text",
17423
+ text: JSON.stringify(
17424
+ { ...doc.frontmatter, content: doc.content },
17425
+ null,
17426
+ 2
17427
+ )
17428
+ }
17429
+ ]
17430
+ };
17431
+ },
17432
+ { annotations: { readOnly: true } }
17433
+ ),
17434
+ // --- Jira → Local tools ---
17435
+ tool19(
17436
+ "pull_jira_issue",
17437
+ "Fetch a single Jira issue by key and create/update a local JI-xxx document",
17438
+ {
17439
+ key: external_exports.string().describe("Jira issue key (e.g. 'PROJ-123')")
17440
+ },
17441
+ async (args) => {
17442
+ const client = createJiraClient();
17443
+ if (!client) return jiraNotConfiguredError();
17444
+ const issue2 = await client.getIssue(args.key);
17445
+ const host = process.env.JIRA_HOST;
17446
+ const existing = findByJiraKey(store, args.key);
17447
+ if (existing) {
17448
+ const fm2 = jiraIssueToFrontmatter(
17449
+ issue2,
17450
+ host,
17451
+ existing.frontmatter.linkedArtifacts
17452
+ );
17453
+ const doc2 = store.update(
17454
+ existing.frontmatter.id,
17455
+ fm2,
17456
+ issue2.fields.description ?? ""
17457
+ );
17458
+ return {
17459
+ content: [
17460
+ {
17461
+ type: "text",
17462
+ text: `Updated ${doc2.frontmatter.id} from Jira ${args.key}`
17463
+ }
17464
+ ]
17465
+ };
17466
+ }
17467
+ const fm = jiraIssueToFrontmatter(issue2, host);
17468
+ const doc = store.create(
17469
+ JIRA_TYPE,
17470
+ fm,
17471
+ issue2.fields.description ?? ""
17472
+ );
17473
+ return {
17474
+ content: [
17475
+ {
17476
+ type: "text",
17477
+ text: `Created ${doc.frontmatter.id} from Jira ${args.key}`
17478
+ }
17479
+ ]
17480
+ };
17481
+ }
17482
+ ),
17483
+ tool19(
17484
+ "pull_jira_issues_jql",
17485
+ "Bulk fetch Jira issues via JQL query and create/update local JI-xxx documents",
17486
+ {
17487
+ jql: external_exports.string().describe(`JQL query (e.g. 'project = PROJ AND status = "In Progress"')`),
17488
+ maxResults: external_exports.number().optional().describe("Max issues to fetch (default 50)")
17489
+ },
17490
+ async (args) => {
17491
+ const client = createJiraClient();
17492
+ if (!client) return jiraNotConfiguredError();
17493
+ const result = await client.searchIssues(args.jql, args.maxResults);
17494
+ const host = process.env.JIRA_HOST;
17495
+ const created = [];
17496
+ const updated = [];
17497
+ for (const issue2 of result.issues) {
17498
+ const existing = findByJiraKey(store, issue2.key);
17499
+ if (existing) {
17500
+ const fm = jiraIssueToFrontmatter(
17501
+ issue2,
17502
+ host,
17503
+ existing.frontmatter.linkedArtifacts
17504
+ );
17505
+ store.update(
17506
+ existing.frontmatter.id,
17507
+ fm,
17508
+ issue2.fields.description ?? ""
17509
+ );
17510
+ updated.push(`${existing.frontmatter.id} (${issue2.key})`);
17511
+ } else {
17512
+ const fm = jiraIssueToFrontmatter(issue2, host);
17513
+ const doc = store.create(
17514
+ JIRA_TYPE,
17515
+ fm,
17516
+ issue2.fields.description ?? ""
17517
+ );
17518
+ created.push(`${doc.frontmatter.id} (${issue2.key})`);
17519
+ }
17520
+ }
17521
+ const parts = [
17522
+ `Fetched ${result.issues.length} of ${result.total} matching issues.`
17523
+ ];
17524
+ if (created.length > 0) parts.push(`Created: ${created.join(", ")}`);
17525
+ if (updated.length > 0) parts.push(`Updated: ${updated.join(", ")}`);
17526
+ return {
17527
+ content: [{ type: "text", text: parts.join("\n") }]
17528
+ };
17529
+ }
17530
+ ),
17531
+ // --- Local → Jira tools ---
17532
+ tool19(
17533
+ "push_artifact_to_jira",
17534
+ "Create a Jira issue from any Marvin artifact (D/A/Q/F/E) and create a tracking JI-xxx document",
17535
+ {
17536
+ artifactId: external_exports.string().describe("Marvin artifact ID (e.g. 'D-001', 'F-003', 'E-002')"),
17537
+ projectKey: external_exports.string().describe("Jira project key (e.g. 'PROJ')"),
17538
+ issueType: external_exports.enum(["Story", "Task", "Bug", "Epic"]).optional().describe("Jira issue type (default: 'Task')")
17539
+ },
17540
+ async (args) => {
17541
+ const client = createJiraClient();
17542
+ if (!client) return jiraNotConfiguredError();
17543
+ const artifact = store.get(args.artifactId);
17544
+ if (!artifact) {
17545
+ return {
17546
+ content: [
17547
+ { type: "text", text: `Artifact ${args.artifactId} not found` }
17548
+ ],
17549
+ isError: true
17550
+ };
17551
+ }
17552
+ const description = [
17553
+ artifact.content,
17554
+ "",
17555
+ `---`,
17556
+ `Marvin artifact: ${artifact.frontmatter.id} (${artifact.frontmatter.type})`,
17557
+ `Status: ${artifact.frontmatter.status}`
17558
+ ].join("\n");
17559
+ const jiraResult = await client.createIssue({
17560
+ project: { key: args.projectKey },
17561
+ summary: artifact.frontmatter.title,
17562
+ description,
17563
+ issuetype: { name: args.issueType ?? "Task" }
17564
+ });
17565
+ const host = process.env.JIRA_HOST;
17566
+ const jiDoc = store.create(
17567
+ JIRA_TYPE,
17568
+ {
17569
+ title: artifact.frontmatter.title,
17570
+ status: "open",
17571
+ jiraKey: jiraResult.key,
17572
+ jiraUrl: `https://${host}/browse/${jiraResult.key}`,
17573
+ issueType: args.issueType ?? "Task",
17574
+ priority: "Medium",
17575
+ assignee: "",
17576
+ labels: [],
17577
+ linkedArtifacts: [args.artifactId],
17578
+ tags: [`jira:${jiraResult.key}`],
17579
+ lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString()
17580
+ },
17581
+ ""
17582
+ );
17583
+ return {
17584
+ content: [
17585
+ {
17586
+ type: "text",
17587
+ text: `Created Jira ${jiraResult.key} from ${args.artifactId}. Tracking locally as ${jiDoc.frontmatter.id}.`
17588
+ }
17589
+ ]
17590
+ };
17591
+ }
17592
+ ),
17593
+ // --- Bidirectional sync ---
17594
+ tool19(
17595
+ "sync_jira_issue",
17596
+ "Bidirectional sync: push local title/description to Jira, pull latest status/assignee/labels back",
17597
+ {
17598
+ id: external_exports.string().describe("Local JI-xxx ID")
17599
+ },
17600
+ async (args) => {
17601
+ const client = createJiraClient();
17602
+ if (!client) return jiraNotConfiguredError();
17603
+ const doc = store.get(args.id);
17604
+ if (!doc || doc.frontmatter.type !== JIRA_TYPE) {
17605
+ return {
17606
+ content: [
17607
+ { type: "text", text: `Jira issue ${args.id} not found locally` }
17608
+ ],
17609
+ isError: true
17610
+ };
17611
+ }
17612
+ const jiraKey = doc.frontmatter.jiraKey;
17613
+ await client.updateIssue(jiraKey, {
17614
+ summary: doc.frontmatter.title,
17615
+ description: doc.content || void 0
17616
+ });
17617
+ const issue2 = await client.getIssue(jiraKey);
17618
+ const host = process.env.JIRA_HOST;
17619
+ const fm = jiraIssueToFrontmatter(
17620
+ issue2,
17621
+ host,
17622
+ doc.frontmatter.linkedArtifacts
17623
+ );
17624
+ store.update(args.id, fm, issue2.fields.description ?? "");
17625
+ return {
17626
+ content: [
17627
+ {
17628
+ type: "text",
17629
+ text: `Synced ${args.id} \u2194 ${jiraKey}. Status: ${fm.status}, Assignee: ${fm.assignee || "unassigned"}`
17630
+ }
17631
+ ]
17632
+ };
17633
+ }
17634
+ ),
17635
+ // --- Local link tool ---
17636
+ tool19(
17637
+ "link_artifact_to_jira",
17638
+ "Add a Marvin artifact ID to a JI-xxx document's linkedArtifacts field",
17639
+ {
17640
+ jiraIssueId: external_exports.string().describe("Local JI-xxx ID"),
17641
+ artifactId: external_exports.string().describe("Marvin artifact ID to link (e.g. 'D-001', 'F-003')")
17642
+ },
17643
+ async (args) => {
17644
+ const doc = store.get(args.jiraIssueId);
17645
+ if (!doc || doc.frontmatter.type !== JIRA_TYPE) {
17646
+ return {
17647
+ content: [
17648
+ {
17649
+ type: "text",
17650
+ text: `Jira issue ${args.jiraIssueId} not found locally`
17651
+ }
17652
+ ],
17653
+ isError: true
17654
+ };
17655
+ }
17656
+ const artifact = store.get(args.artifactId);
17657
+ if (!artifact) {
17658
+ return {
17659
+ content: [
17660
+ { type: "text", text: `Artifact ${args.artifactId} not found` }
17661
+ ],
17662
+ isError: true
17663
+ };
17664
+ }
17665
+ const linked = doc.frontmatter.linkedArtifacts ?? [];
17666
+ if (linked.includes(args.artifactId)) {
17667
+ return {
17668
+ content: [
17669
+ {
17670
+ type: "text",
17671
+ text: `${args.artifactId} is already linked to ${args.jiraIssueId}`
17672
+ }
17673
+ ]
17674
+ };
17675
+ }
17676
+ store.update(args.jiraIssueId, {
17677
+ linkedArtifacts: [...linked, args.artifactId]
17678
+ });
17679
+ return {
17680
+ content: [
17681
+ {
17682
+ type: "text",
17683
+ text: `Linked ${args.artifactId} to ${args.jiraIssueId}`
17684
+ }
17685
+ ]
17686
+ };
17687
+ }
17688
+ )
17689
+ ];
17690
+ }
17691
+
17692
+ // src/skills/builtin/jira/index.ts
17693
+ var jiraSkill = {
17694
+ id: "jira",
17695
+ name: "Jira Integration",
17696
+ description: "Bidirectional sync between Marvin artifacts and Jira issues",
17697
+ version: "1.0.0",
17698
+ format: "builtin-ts",
17699
+ // No default persona affinity — opt-in via config.yaml skills section
17700
+ documentTypeRegistrations: [
17701
+ { type: "jira-issue", dirName: "jira-issues", idPrefix: "JI" }
17702
+ ],
17703
+ tools: (store) => createJiraTools(store),
17704
+ promptFragments: {
17705
+ "product-owner": `You have the **Jira Integration** skill. You can pull issues from Jira and push Marvin artifacts to Jira.
17706
+
17707
+ **Available tools:**
17708
+ - \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues
17709
+ - \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import issues from Jira by key or JQL query
17710
+ - \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, feature, etc.)
17711
+ - \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
17712
+ - \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx
17713
+
17714
+ **As Product Owner, use Jira integration to:**
17715
+ - Pull stakeholder-reported issues for triage and prioritization
17716
+ - Push approved features as Stories for development tracking
17717
+ - Link decisions to Jira issues for audit trail and traceability
17718
+ - Use JQL queries to review backlog status (e.g. \`project = PROJ AND status = "To Do"\`)`,
17719
+ "tech-lead": `You have the **Jira Integration** skill. You can pull issues from Jira and push Marvin artifacts to Jira.
17720
+
17721
+ **Available tools:**
17722
+ - \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues
17723
+ - \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import issues from Jira by key or JQL query
17724
+ - \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, action, epic, etc.)
17725
+ - \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
17726
+ - \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx
17727
+
17728
+ **As Tech Lead, use Jira integration to:**
17729
+ - Pull technical issues and bugs for sprint planning and estimation
17730
+ - Push epics and technical decisions to Jira for cross-team visibility
17731
+ - Bidirectional sync to keep local governance and Jira in alignment
17732
+ - Use JQL queries to track technical debt (e.g. \`labels = "tech-debt" AND status != "Done"\`)`,
17733
+ "delivery-manager": `You have the **Jira Integration** skill. You can pull issues from Jira and push Marvin artifacts to Jira.
17734
+
17735
+ **Available tools:**
17736
+ - \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues
17737
+ - \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import issues from Jira by key or JQL query
17738
+ - \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, action, etc.)
17739
+ - \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
17740
+ - \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx
17741
+
17742
+ **As Delivery Manager, use Jira integration to:**
17743
+ - Pull sprint issues for tracking progress and blockers
17744
+ - Push actions and decisions to Jira for stakeholder visibility
17745
+ - Use JQL queries for reporting (e.g. \`sprint in openSprints() AND assignee = currentUser()\`)
17746
+ - Sync status between Marvin governance items and Jira issues`
17747
+ }
17748
+ };
17749
+
17174
17750
  // src/skills/registry.ts
17175
17751
  var BUILTIN_SKILLS = {
17176
- "governance-review": governanceReviewSkill
17752
+ "governance-review": governanceReviewSkill,
17753
+ "jira": jiraSkill
17177
17754
  };
17178
17755
  function getBuiltinSkillsDir() {
17179
17756
  const thisFile = fileURLToPath(import.meta.url);
@@ -17330,7 +17907,7 @@ ${fragment}`);
17330
17907
  }
17331
17908
 
17332
17909
  // src/skills/action-tools.ts
17333
- import { tool as tool19 } from "@anthropic-ai/claude-agent-sdk";
17910
+ import { tool as tool20 } from "@anthropic-ai/claude-agent-sdk";
17334
17911
 
17335
17912
  // src/skills/action-runner.ts
17336
17913
  import { query } from "@anthropic-ai/claude-agent-sdk";
@@ -17420,7 +17997,7 @@ function createSkillActionTools(skills, context) {
17420
17997
  if (!skill.actions) continue;
17421
17998
  for (const action of skill.actions) {
17422
17999
  tools.push(
17423
- tool19(
18000
+ tool20(
17424
18001
  `${skill.id}__${action.id}`,
17425
18002
  action.description,
17426
18003
  {
@@ -17647,10 +18224,10 @@ ${lines.join("\n\n")}`;
17647
18224
  }
17648
18225
 
17649
18226
  // src/mcp/persona-tools.ts
17650
- import { tool as tool20 } from "@anthropic-ai/claude-agent-sdk";
18227
+ import { tool as tool21 } from "@anthropic-ai/claude-agent-sdk";
17651
18228
  function createPersonaTools(ctx, marvinDir) {
17652
18229
  return [
17653
- tool20(
18230
+ tool21(
17654
18231
  "set_persona",
17655
18232
  "Set the active persona for this session. Returns full guidance for the selected persona including behavioral rules, allowed document types, and scope. Call this before working to ensure persona-appropriate behavior.",
17656
18233
  {
@@ -17680,7 +18257,7 @@ ${summaries}`
17680
18257
  };
17681
18258
  }
17682
18259
  ),
17683
- tool20(
18260
+ tool21(
17684
18261
  "get_persona_guidance",
17685
18262
  "Get guidance for a persona without changing the active persona. If no persona is specified, lists all available personas with summaries.",
17686
18263
  {