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.
- package/README.md +89 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +750 -67
- package/dist/index.js.map +1 -1
- package/dist/marvin-serve.js +617 -40
- package/dist/marvin-serve.js.map +1 -1
- package/dist/marvin.js +750 -67
- 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 } }
|
|
@@ -17502,9 +17593,495 @@ Be thorough but concise. Focus on actionable insights.`,
|
|
|
17502
17593
|
]
|
|
17503
17594
|
};
|
|
17504
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
|
+
|
|
17505
18081
|
// src/skills/registry.ts
|
|
17506
18082
|
var BUILTIN_SKILLS = {
|
|
17507
|
-
"governance-review": governanceReviewSkill
|
|
18083
|
+
"governance-review": governanceReviewSkill,
|
|
18084
|
+
"jira": jiraSkill
|
|
17508
18085
|
};
|
|
17509
18086
|
var GOVERNANCE_TOOL_NAMES = [
|
|
17510
18087
|
"mcp__marvin-governance__list_decisions",
|
|
@@ -17653,6 +18230,16 @@ function resolveSkillsForPersona(personaId, skillsConfig, allSkills) {
|
|
|
17653
18230
|
}
|
|
17654
18231
|
return result;
|
|
17655
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
|
+
}
|
|
17656
18243
|
function getSkillTools(skillIds, allSkills, store) {
|
|
17657
18244
|
const tools = [];
|
|
17658
18245
|
for (const id of skillIds) {
|
|
@@ -17764,16 +18351,17 @@ ${wildcardPrompt}
|
|
|
17764
18351
|
async function startSession(options) {
|
|
17765
18352
|
const { persona, config: config2, marvinDir, projectRoot } = options;
|
|
17766
18353
|
const plugin = resolvePlugin(config2.project.methodology);
|
|
17767
|
-
const
|
|
17768
|
-
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]);
|
|
17769
18359
|
const sessionStore = new SessionStore(marvinDir);
|
|
17770
18360
|
const sourcesDir = path8.join(marvinDir, "sources");
|
|
17771
18361
|
const hasSourcesDir = fs8.existsSync(sourcesDir);
|
|
17772
18362
|
const manifest = hasSourcesDir ? new SourceManifestManager(marvinDir) : void 0;
|
|
17773
18363
|
const pluginTools = plugin ? getPluginTools(plugin, store, marvinDir) : [];
|
|
17774
18364
|
const pluginPromptFragment = plugin ? getPluginPromptFragment(plugin, persona.id) : void 0;
|
|
17775
|
-
const allSkills = loadAllSkills(marvinDir);
|
|
17776
|
-
const skillIds = resolveSkillsForPersona(persona.id, config2.project.skills, allSkills);
|
|
17777
18365
|
const codeSkillTools = getSkillTools(skillIds, allSkills, store);
|
|
17778
18366
|
const skillAgents = getSkillAgentDefinitions(skillIds, allSkills);
|
|
17779
18367
|
const skillPromptFragment = getSkillPromptFragment(skillIds, allSkills, persona.id);
|
|
@@ -17970,7 +18558,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
17970
18558
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
17971
18559
|
|
|
17972
18560
|
// src/skills/action-tools.ts
|
|
17973
|
-
import { tool as
|
|
18561
|
+
import { tool as tool20 } from "@anthropic-ai/claude-agent-sdk";
|
|
17974
18562
|
|
|
17975
18563
|
// src/skills/action-runner.ts
|
|
17976
18564
|
import { query as query3 } from "@anthropic-ai/claude-agent-sdk";
|
|
@@ -18036,7 +18624,7 @@ function createSkillActionTools(skills, context) {
|
|
|
18036
18624
|
if (!skill.actions) continue;
|
|
18037
18625
|
for (const action of skill.actions) {
|
|
18038
18626
|
tools.push(
|
|
18039
|
-
|
|
18627
|
+
tool20(
|
|
18040
18628
|
`${skill.id}__${action.id}`,
|
|
18041
18629
|
action.description,
|
|
18042
18630
|
{
|
|
@@ -18128,10 +18716,10 @@ ${lines.join("\n\n")}`;
|
|
|
18128
18716
|
}
|
|
18129
18717
|
|
|
18130
18718
|
// src/mcp/persona-tools.ts
|
|
18131
|
-
import { tool as
|
|
18719
|
+
import { tool as tool21 } from "@anthropic-ai/claude-agent-sdk";
|
|
18132
18720
|
function createPersonaTools(ctx, marvinDir) {
|
|
18133
18721
|
return [
|
|
18134
|
-
|
|
18722
|
+
tool21(
|
|
18135
18723
|
"set_persona",
|
|
18136
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.",
|
|
18137
18725
|
{
|
|
@@ -18161,7 +18749,7 @@ ${summaries}`
|
|
|
18161
18749
|
};
|
|
18162
18750
|
}
|
|
18163
18751
|
),
|
|
18164
|
-
|
|
18752
|
+
tool21(
|
|
18165
18753
|
"get_persona_guidance",
|
|
18166
18754
|
"Get guidance for a persona without changing the active persona. If no persona is specified, lists all available personas with summaries.",
|
|
18167
18755
|
{
|
|
@@ -19404,20 +19992,23 @@ async function skillsInstallCommand(skillId, options) {
|
|
|
19404
19992
|
console.log(chalk10.red("Please specify a persona with --as <persona>."));
|
|
19405
19993
|
return;
|
|
19406
19994
|
}
|
|
19995
|
+
const targets = persona === "all" ? listPersonas().map((p) => p.id) : [persona];
|
|
19407
19996
|
const config2 = loadProjectConfig(project.marvinDir);
|
|
19408
19997
|
if (!config2.skills) {
|
|
19409
19998
|
config2.skills = {};
|
|
19410
19999
|
}
|
|
19411
|
-
|
|
19412
|
-
config2.skills[
|
|
19413
|
-
|
|
19414
|
-
|
|
19415
|
-
|
|
19416
|
-
|
|
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}.`));
|
|
19417
20010
|
}
|
|
19418
|
-
config2.skills[persona].push(skillId);
|
|
19419
20011
|
saveProjectConfig(project.marvinDir, config2);
|
|
19420
|
-
console.log(chalk10.green(`Assigned skill "${skillId}" to ${persona}.`));
|
|
19421
20012
|
}
|
|
19422
20013
|
async function skillsRemoveCommand(skillId, options) {
|
|
19423
20014
|
const project = loadProject();
|
|
@@ -19426,25 +20017,28 @@ async function skillsRemoveCommand(skillId, options) {
|
|
|
19426
20017
|
console.log(chalk10.red("Please specify a persona with --as <persona>."));
|
|
19427
20018
|
return;
|
|
19428
20019
|
}
|
|
20020
|
+
const targets = persona === "all" ? listPersonas().map((p) => p.id) : [persona];
|
|
19429
20021
|
const config2 = loadProjectConfig(project.marvinDir);
|
|
19430
|
-
|
|
19431
|
-
|
|
19432
|
-
|
|
19433
|
-
|
|
19434
|
-
|
|
19435
|
-
|
|
19436
|
-
|
|
19437
|
-
|
|
19438
|
-
|
|
19439
|
-
|
|
19440
|
-
|
|
19441
|
-
|
|
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}.`));
|
|
19442
20037
|
}
|
|
19443
|
-
if (Object.keys(config2.skills).length === 0) {
|
|
20038
|
+
if (config2.skills && Object.keys(config2.skills).length === 0) {
|
|
19444
20039
|
delete config2.skills;
|
|
19445
20040
|
}
|
|
19446
20041
|
saveProjectConfig(project.marvinDir, config2);
|
|
19447
|
-
console.log(chalk10.green(`Removed skill "${skillId}" from ${persona}.`));
|
|
19448
20042
|
}
|
|
19449
20043
|
async function skillsCreateCommand(name) {
|
|
19450
20044
|
const project = loadProject();
|
|
@@ -20691,12 +21285,94 @@ Contribution: ${options.type}`));
|
|
|
20691
21285
|
});
|
|
20692
21286
|
}
|
|
20693
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
|
+
|
|
20694
21370
|
// src/cli/program.ts
|
|
20695
21371
|
function createProgram() {
|
|
20696
21372
|
const program = new Command();
|
|
20697
21373
|
program.name("marvin").description(
|
|
20698
21374
|
"AI-powered product development assistant with Product Owner, Delivery Manager, and Technical Lead personas"
|
|
20699
|
-
).version("0.2.
|
|
21375
|
+
).version("0.2.8");
|
|
20700
21376
|
program.command("init").description("Initialize a new Marvin project in the current directory").action(async () => {
|
|
20701
21377
|
await initCommand();
|
|
20702
21378
|
});
|
|
@@ -20766,6 +21442,13 @@ function createProgram() {
|
|
|
20766
21442
|
skillsCmd.command("migrate").description("Migrate YAML skill files to SKILL.md directory format").action(async () => {
|
|
20767
21443
|
await skillsMigrateCommand();
|
|
20768
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
|
+
});
|
|
20769
21452
|
return program;
|
|
20770
21453
|
}
|
|
20771
21454
|
export {
|