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/dist/marvin.js CHANGED
@@ -14101,6 +14101,128 @@ function createMeetingTools(store) {
14101
14101
 
14102
14102
  // src/plugins/builtin/tools/reports.ts
14103
14103
  import { tool as tool2 } from "@anthropic-ai/claude-agent-sdk";
14104
+
14105
+ // src/reports/gar/collector.ts
14106
+ function collectGarMetrics(store) {
14107
+ const allActions = store.list({ type: "action" });
14108
+ const openActions = allActions.filter((d) => d.frontmatter.status === "open");
14109
+ const doneActions = allActions.filter((d) => d.frontmatter.status === "done");
14110
+ const allDocs = store.list();
14111
+ const blockedItems = allDocs.filter(
14112
+ (d) => d.frontmatter.tags?.includes("blocked")
14113
+ );
14114
+ const overdueItems = allDocs.filter(
14115
+ (d) => d.frontmatter.tags?.includes("overdue")
14116
+ );
14117
+ const openQuestions = store.list({ type: "question", status: "open" });
14118
+ const riskItems = allDocs.filter(
14119
+ (d) => d.frontmatter.tags?.includes("risk")
14120
+ );
14121
+ const unownedActions = openActions.filter((d) => !d.frontmatter.owner);
14122
+ const total = allActions.length;
14123
+ const done = doneActions.length;
14124
+ const completionPct = total > 0 ? Math.round(done / total * 100) : 100;
14125
+ const scheduleItems = [
14126
+ ...blockedItems,
14127
+ ...overdueItems
14128
+ ].filter(
14129
+ (d, i, arr) => arr.findIndex((x) => x.frontmatter.id === d.frontmatter.id) === i
14130
+ ).map((d) => ({ id: d.frontmatter.id, title: d.frontmatter.title }));
14131
+ const qualityItems = [
14132
+ ...riskItems,
14133
+ ...openQuestions
14134
+ ].filter(
14135
+ (d, i, arr) => arr.findIndex((x) => x.frontmatter.id === d.frontmatter.id) === i
14136
+ ).map((d) => ({ id: d.frontmatter.id, title: d.frontmatter.title }));
14137
+ const resourceItems = unownedActions.map((d) => ({
14138
+ id: d.frontmatter.id,
14139
+ title: d.frontmatter.title
14140
+ }));
14141
+ return {
14142
+ scope: {
14143
+ total,
14144
+ open: openActions.length,
14145
+ done,
14146
+ completionPct
14147
+ },
14148
+ schedule: {
14149
+ blocked: blockedItems.length,
14150
+ overdue: overdueItems.length,
14151
+ items: scheduleItems
14152
+ },
14153
+ quality: {
14154
+ risks: riskItems.length,
14155
+ openQuestions: openQuestions.length,
14156
+ items: qualityItems
14157
+ },
14158
+ resources: {
14159
+ unowned: unownedActions.length,
14160
+ items: resourceItems
14161
+ }
14162
+ };
14163
+ }
14164
+
14165
+ // src/reports/gar/evaluator.ts
14166
+ function worstStatus(statuses) {
14167
+ if (statuses.includes("red")) return "red";
14168
+ if (statuses.includes("amber")) return "amber";
14169
+ return "green";
14170
+ }
14171
+ function evaluateGar(projectName, metrics) {
14172
+ const areas = [];
14173
+ const scopePct = metrics.scope.completionPct;
14174
+ const scopeStatus = scopePct >= 70 ? "green" : scopePct >= 40 ? "amber" : "red";
14175
+ areas.push({
14176
+ name: "Scope",
14177
+ status: scopeStatus,
14178
+ summary: `${scopePct}% complete (${metrics.scope.done}/${metrics.scope.total})`,
14179
+ items: []
14180
+ });
14181
+ const scheduleCount = metrics.schedule.blocked + metrics.schedule.overdue;
14182
+ const scheduleStatus = scheduleCount === 0 ? "green" : scheduleCount <= 2 ? "amber" : "red";
14183
+ const scheduleParts = [];
14184
+ if (metrics.schedule.blocked > 0)
14185
+ scheduleParts.push(`${metrics.schedule.blocked} blocked`);
14186
+ if (metrics.schedule.overdue > 0)
14187
+ scheduleParts.push(`${metrics.schedule.overdue} overdue`);
14188
+ areas.push({
14189
+ name: "Schedule",
14190
+ status: scheduleStatus,
14191
+ summary: scheduleParts.length > 0 ? scheduleParts.join(", ") : "on track",
14192
+ items: metrics.schedule.items
14193
+ });
14194
+ const qualityCount = metrics.quality.risks + metrics.quality.openQuestions;
14195
+ const qualityStatus = qualityCount === 0 ? "green" : qualityCount <= 2 ? "amber" : "red";
14196
+ const qualityParts = [];
14197
+ if (metrics.quality.risks > 0)
14198
+ qualityParts.push(`${metrics.quality.risks} risk(s)`);
14199
+ if (metrics.quality.openQuestions > 0)
14200
+ qualityParts.push(`${metrics.quality.openQuestions} open question(s)`);
14201
+ areas.push({
14202
+ name: "Quality",
14203
+ status: qualityStatus,
14204
+ summary: qualityParts.length > 0 ? qualityParts.join(", ") : "no issues",
14205
+ items: metrics.quality.items
14206
+ });
14207
+ const resourceCount = metrics.resources.unowned;
14208
+ const resourceStatus = resourceCount === 0 ? "green" : resourceCount <= 2 ? "amber" : "red";
14209
+ areas.push({
14210
+ name: "Resources",
14211
+ status: resourceStatus,
14212
+ summary: resourceCount > 0 ? `${resourceCount} unowned action(s)` : "all assigned",
14213
+ items: metrics.resources.items
14214
+ });
14215
+ const overall = worstStatus(areas.map((a) => a.status));
14216
+ return {
14217
+ projectName,
14218
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
14219
+ overall,
14220
+ areas,
14221
+ metrics
14222
+ };
14223
+ }
14224
+
14225
+ // src/plugins/builtin/tools/reports.ts
14104
14226
  function createReportTools(store) {
14105
14227
  return [
14106
14228
  tool2(
@@ -14189,41 +14311,10 @@ function createReportTools(store) {
14189
14311
  "Generate a Green-Amber-Red report with metrics across scope, schedule, quality, and resources",
14190
14312
  {},
14191
14313
  async () => {
14192
- const allActions = store.list({ type: "action" });
14193
- const openActions = allActions.filter((d) => d.frontmatter.status === "open");
14194
- const doneActions = allActions.filter((d) => d.frontmatter.status === "done");
14195
- const allDocs = store.list();
14196
- const blockedItems = allDocs.filter(
14197
- (d) => d.frontmatter.tags?.includes("blocked")
14198
- );
14199
- const overdueItems = allDocs.filter(
14200
- (d) => d.frontmatter.tags?.includes("overdue")
14201
- );
14202
- const openQuestions = store.list({ type: "question", status: "open" });
14203
- const riskItems = allDocs.filter(
14204
- (d) => d.frontmatter.tags?.includes("risk")
14205
- );
14206
- const unownedActions = openActions.filter((d) => !d.frontmatter.owner);
14207
- const areas = {
14208
- scope: {
14209
- total: allActions.length,
14210
- open: openActions.length,
14211
- done: doneActions.length
14212
- },
14213
- schedule: {
14214
- blocked: blockedItems.length,
14215
- overdue: overdueItems.length
14216
- },
14217
- quality: {
14218
- openQuestions: openQuestions.length,
14219
- risks: riskItems.length
14220
- },
14221
- resources: {
14222
- unowned: unownedActions.length
14223
- }
14224
- };
14314
+ const metrics = collectGarMetrics(store);
14315
+ const report = evaluateGar("project", metrics);
14225
14316
  return {
14226
- content: [{ type: "text", text: JSON.stringify({ areas }, null, 2) }]
14317
+ content: [{ type: "text", text: JSON.stringify(report, null, 2) }]
14227
14318
  };
14228
14319
  },
14229
14320
  { annotations: { readOnly: true } }
@@ -17615,9 +17706,495 @@ Be thorough but concise. Focus on actionable insights.`,
17615
17706
  ]
17616
17707
  };
17617
17708
 
17709
+ // src/skills/builtin/jira/tools.ts
17710
+ import { tool as tool19 } from "@anthropic-ai/claude-agent-sdk";
17711
+
17712
+ // src/skills/builtin/jira/client.ts
17713
+ var JiraClient = class {
17714
+ baseUrl;
17715
+ authHeader;
17716
+ constructor(config2) {
17717
+ this.baseUrl = `https://${config2.host}/rest/api/2`;
17718
+ this.authHeader = "Basic " + Buffer.from(`${config2.email}:${config2.apiToken}`).toString("base64");
17719
+ }
17720
+ async request(path18, method = "GET", body) {
17721
+ const url2 = `${this.baseUrl}${path18}`;
17722
+ const headers = {
17723
+ Authorization: this.authHeader,
17724
+ "Content-Type": "application/json",
17725
+ Accept: "application/json"
17726
+ };
17727
+ const response = await fetch(url2, {
17728
+ method,
17729
+ headers,
17730
+ body: body ? JSON.stringify(body) : void 0
17731
+ });
17732
+ if (!response.ok) {
17733
+ const text = await response.text().catch(() => "");
17734
+ throw new Error(
17735
+ `Jira API error ${response.status} ${method} ${path18}: ${text}`
17736
+ );
17737
+ }
17738
+ if (response.status === 204) return void 0;
17739
+ return response.json();
17740
+ }
17741
+ async searchIssues(jql, maxResults = 50) {
17742
+ const params = new URLSearchParams({
17743
+ jql,
17744
+ maxResults: String(maxResults)
17745
+ });
17746
+ return this.request(`/search?${params}`);
17747
+ }
17748
+ async getIssue(key) {
17749
+ return this.request(`/issue/${encodeURIComponent(key)}`);
17750
+ }
17751
+ async createIssue(fields) {
17752
+ return this.request("/issue", "POST", { fields });
17753
+ }
17754
+ async updateIssue(key, fields) {
17755
+ await this.request(
17756
+ `/issue/${encodeURIComponent(key)}`,
17757
+ "PUT",
17758
+ { fields }
17759
+ );
17760
+ }
17761
+ async addComment(key, body) {
17762
+ await this.request(
17763
+ `/issue/${encodeURIComponent(key)}/comment`,
17764
+ "POST",
17765
+ { body }
17766
+ );
17767
+ }
17768
+ };
17769
+ function createJiraClient() {
17770
+ const host = process.env.JIRA_HOST;
17771
+ const email3 = process.env.JIRA_EMAIL;
17772
+ const apiToken = process.env.JIRA_API_TOKEN;
17773
+ if (!host || !email3 || !apiToken) return null;
17774
+ return new JiraClient({ host, email: email3, apiToken });
17775
+ }
17776
+
17777
+ // src/skills/builtin/jira/tools.ts
17778
+ var JIRA_TYPE = "jira-issue";
17779
+ function jiraNotConfiguredError() {
17780
+ return {
17781
+ content: [
17782
+ {
17783
+ type: "text",
17784
+ text: "Jira is not configured. Set JIRA_HOST, JIRA_EMAIL, and JIRA_API_TOKEN environment variables."
17785
+ }
17786
+ ],
17787
+ isError: true
17788
+ };
17789
+ }
17790
+ function mapJiraStatus(jiraStatus) {
17791
+ const lower = jiraStatus.toLowerCase();
17792
+ if (lower === "done" || lower === "closed" || lower === "resolved") return "done";
17793
+ if (lower === "in progress" || lower === "in review") return "in-progress";
17794
+ return "open";
17795
+ }
17796
+ function jiraIssueToFrontmatter(issue2, host, linkedArtifacts) {
17797
+ return {
17798
+ title: issue2.fields.summary,
17799
+ status: mapJiraStatus(issue2.fields.status.name),
17800
+ jiraKey: issue2.key,
17801
+ jiraUrl: `https://${host}/browse/${issue2.key}`,
17802
+ issueType: issue2.fields.issuetype.name,
17803
+ priority: issue2.fields.priority?.name ?? "None",
17804
+ assignee: issue2.fields.assignee?.displayName ?? "",
17805
+ labels: issue2.fields.labels ?? [],
17806
+ linkedArtifacts: linkedArtifacts ?? [],
17807
+ tags: [`jira:${issue2.key}`],
17808
+ lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString()
17809
+ };
17810
+ }
17811
+ function findByJiraKey(store, jiraKey) {
17812
+ const docs = store.list({ type: JIRA_TYPE });
17813
+ return docs.find((d) => d.frontmatter.jiraKey === jiraKey);
17814
+ }
17815
+ function createJiraTools(store) {
17816
+ return [
17817
+ // --- Local read tools ---
17818
+ tool19(
17819
+ "list_jira_issues",
17820
+ "List locally synced Jira issues (JI-xxx documents), optionally filtered by status or Jira key",
17821
+ {
17822
+ status: external_exports.enum(["open", "in-progress", "done"]).optional().describe("Filter by local status"),
17823
+ jiraKey: external_exports.string().optional().describe("Filter by Jira issue key (e.g. 'PROJ-123')")
17824
+ },
17825
+ async (args) => {
17826
+ let docs = store.list({ type: JIRA_TYPE, status: args.status });
17827
+ if (args.jiraKey) {
17828
+ docs = docs.filter((d) => d.frontmatter.jiraKey === args.jiraKey);
17829
+ }
17830
+ const summary = docs.map((d) => ({
17831
+ id: d.frontmatter.id,
17832
+ title: d.frontmatter.title,
17833
+ status: d.frontmatter.status,
17834
+ jiraKey: d.frontmatter.jiraKey,
17835
+ issueType: d.frontmatter.issueType,
17836
+ priority: d.frontmatter.priority,
17837
+ assignee: d.frontmatter.assignee,
17838
+ linkedArtifacts: d.frontmatter.linkedArtifacts
17839
+ }));
17840
+ return {
17841
+ content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
17842
+ };
17843
+ },
17844
+ { annotations: { readOnly: true } }
17845
+ ),
17846
+ tool19(
17847
+ "get_jira_issue",
17848
+ "Get the full content of a locally synced Jira issue by local ID (JI-xxx) or Jira key (PROJ-123)",
17849
+ {
17850
+ id: external_exports.string().describe("Local ID (e.g. 'JI-001') or Jira key (e.g. 'PROJ-123')")
17851
+ },
17852
+ async (args) => {
17853
+ let doc = store.get(args.id);
17854
+ if (!doc) {
17855
+ doc = findByJiraKey(store, args.id);
17856
+ }
17857
+ if (!doc) {
17858
+ return {
17859
+ content: [{ type: "text", text: `Jira issue ${args.id} not found locally` }],
17860
+ isError: true
17861
+ };
17862
+ }
17863
+ return {
17864
+ content: [
17865
+ {
17866
+ type: "text",
17867
+ text: JSON.stringify(
17868
+ { ...doc.frontmatter, content: doc.content },
17869
+ null,
17870
+ 2
17871
+ )
17872
+ }
17873
+ ]
17874
+ };
17875
+ },
17876
+ { annotations: { readOnly: true } }
17877
+ ),
17878
+ // --- Jira → Local tools ---
17879
+ tool19(
17880
+ "pull_jira_issue",
17881
+ "Fetch a single Jira issue by key and create/update a local JI-xxx document",
17882
+ {
17883
+ key: external_exports.string().describe("Jira issue key (e.g. 'PROJ-123')")
17884
+ },
17885
+ async (args) => {
17886
+ const client = createJiraClient();
17887
+ if (!client) return jiraNotConfiguredError();
17888
+ const issue2 = await client.getIssue(args.key);
17889
+ const host = process.env.JIRA_HOST;
17890
+ const existing = findByJiraKey(store, args.key);
17891
+ if (existing) {
17892
+ const fm2 = jiraIssueToFrontmatter(
17893
+ issue2,
17894
+ host,
17895
+ existing.frontmatter.linkedArtifacts
17896
+ );
17897
+ const doc2 = store.update(
17898
+ existing.frontmatter.id,
17899
+ fm2,
17900
+ issue2.fields.description ?? ""
17901
+ );
17902
+ return {
17903
+ content: [
17904
+ {
17905
+ type: "text",
17906
+ text: `Updated ${doc2.frontmatter.id} from Jira ${args.key}`
17907
+ }
17908
+ ]
17909
+ };
17910
+ }
17911
+ const fm = jiraIssueToFrontmatter(issue2, host);
17912
+ const doc = store.create(
17913
+ JIRA_TYPE,
17914
+ fm,
17915
+ issue2.fields.description ?? ""
17916
+ );
17917
+ return {
17918
+ content: [
17919
+ {
17920
+ type: "text",
17921
+ text: `Created ${doc.frontmatter.id} from Jira ${args.key}`
17922
+ }
17923
+ ]
17924
+ };
17925
+ }
17926
+ ),
17927
+ tool19(
17928
+ "pull_jira_issues_jql",
17929
+ "Bulk fetch Jira issues via JQL query and create/update local JI-xxx documents",
17930
+ {
17931
+ jql: external_exports.string().describe(`JQL query (e.g. 'project = PROJ AND status = "In Progress"')`),
17932
+ maxResults: external_exports.number().optional().describe("Max issues to fetch (default 50)")
17933
+ },
17934
+ async (args) => {
17935
+ const client = createJiraClient();
17936
+ if (!client) return jiraNotConfiguredError();
17937
+ const result = await client.searchIssues(args.jql, args.maxResults);
17938
+ const host = process.env.JIRA_HOST;
17939
+ const created = [];
17940
+ const updated = [];
17941
+ for (const issue2 of result.issues) {
17942
+ const existing = findByJiraKey(store, issue2.key);
17943
+ if (existing) {
17944
+ const fm = jiraIssueToFrontmatter(
17945
+ issue2,
17946
+ host,
17947
+ existing.frontmatter.linkedArtifacts
17948
+ );
17949
+ store.update(
17950
+ existing.frontmatter.id,
17951
+ fm,
17952
+ issue2.fields.description ?? ""
17953
+ );
17954
+ updated.push(`${existing.frontmatter.id} (${issue2.key})`);
17955
+ } else {
17956
+ const fm = jiraIssueToFrontmatter(issue2, host);
17957
+ const doc = store.create(
17958
+ JIRA_TYPE,
17959
+ fm,
17960
+ issue2.fields.description ?? ""
17961
+ );
17962
+ created.push(`${doc.frontmatter.id} (${issue2.key})`);
17963
+ }
17964
+ }
17965
+ const parts = [
17966
+ `Fetched ${result.issues.length} of ${result.total} matching issues.`
17967
+ ];
17968
+ if (created.length > 0) parts.push(`Created: ${created.join(", ")}`);
17969
+ if (updated.length > 0) parts.push(`Updated: ${updated.join(", ")}`);
17970
+ return {
17971
+ content: [{ type: "text", text: parts.join("\n") }]
17972
+ };
17973
+ }
17974
+ ),
17975
+ // --- Local → Jira tools ---
17976
+ tool19(
17977
+ "push_artifact_to_jira",
17978
+ "Create a Jira issue from any Marvin artifact (D/A/Q/F/E) and create a tracking JI-xxx document",
17979
+ {
17980
+ artifactId: external_exports.string().describe("Marvin artifact ID (e.g. 'D-001', 'F-003', 'E-002')"),
17981
+ projectKey: external_exports.string().describe("Jira project key (e.g. 'PROJ')"),
17982
+ issueType: external_exports.enum(["Story", "Task", "Bug", "Epic"]).optional().describe("Jira issue type (default: 'Task')")
17983
+ },
17984
+ async (args) => {
17985
+ const client = createJiraClient();
17986
+ if (!client) return jiraNotConfiguredError();
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 description = [
17997
+ artifact.content,
17998
+ "",
17999
+ `---`,
18000
+ `Marvin artifact: ${artifact.frontmatter.id} (${artifact.frontmatter.type})`,
18001
+ `Status: ${artifact.frontmatter.status}`
18002
+ ].join("\n");
18003
+ const jiraResult = await client.createIssue({
18004
+ project: { key: args.projectKey },
18005
+ summary: artifact.frontmatter.title,
18006
+ description,
18007
+ issuetype: { name: args.issueType ?? "Task" }
18008
+ });
18009
+ const host = process.env.JIRA_HOST;
18010
+ const jiDoc = store.create(
18011
+ JIRA_TYPE,
18012
+ {
18013
+ title: artifact.frontmatter.title,
18014
+ status: "open",
18015
+ jiraKey: jiraResult.key,
18016
+ jiraUrl: `https://${host}/browse/${jiraResult.key}`,
18017
+ issueType: args.issueType ?? "Task",
18018
+ priority: "Medium",
18019
+ assignee: "",
18020
+ labels: [],
18021
+ linkedArtifacts: [args.artifactId],
18022
+ tags: [`jira:${jiraResult.key}`],
18023
+ lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString()
18024
+ },
18025
+ ""
18026
+ );
18027
+ return {
18028
+ content: [
18029
+ {
18030
+ type: "text",
18031
+ text: `Created Jira ${jiraResult.key} from ${args.artifactId}. Tracking locally as ${jiDoc.frontmatter.id}.`
18032
+ }
18033
+ ]
18034
+ };
18035
+ }
18036
+ ),
18037
+ // --- Bidirectional sync ---
18038
+ tool19(
18039
+ "sync_jira_issue",
18040
+ "Bidirectional sync: push local title/description to Jira, pull latest status/assignee/labels back",
18041
+ {
18042
+ id: external_exports.string().describe("Local JI-xxx ID")
18043
+ },
18044
+ async (args) => {
18045
+ const client = createJiraClient();
18046
+ if (!client) return jiraNotConfiguredError();
18047
+ const doc = store.get(args.id);
18048
+ if (!doc || doc.frontmatter.type !== JIRA_TYPE) {
18049
+ return {
18050
+ content: [
18051
+ { type: "text", text: `Jira issue ${args.id} not found locally` }
18052
+ ],
18053
+ isError: true
18054
+ };
18055
+ }
18056
+ const jiraKey = doc.frontmatter.jiraKey;
18057
+ await client.updateIssue(jiraKey, {
18058
+ summary: doc.frontmatter.title,
18059
+ description: doc.content || void 0
18060
+ });
18061
+ const issue2 = await client.getIssue(jiraKey);
18062
+ const host = process.env.JIRA_HOST;
18063
+ const fm = jiraIssueToFrontmatter(
18064
+ issue2,
18065
+ host,
18066
+ doc.frontmatter.linkedArtifacts
18067
+ );
18068
+ store.update(args.id, fm, issue2.fields.description ?? "");
18069
+ return {
18070
+ content: [
18071
+ {
18072
+ type: "text",
18073
+ text: `Synced ${args.id} \u2194 ${jiraKey}. Status: ${fm.status}, Assignee: ${fm.assignee || "unassigned"}`
18074
+ }
18075
+ ]
18076
+ };
18077
+ }
18078
+ ),
18079
+ // --- Local link tool ---
18080
+ tool19(
18081
+ "link_artifact_to_jira",
18082
+ "Add a Marvin artifact ID to a JI-xxx document's linkedArtifacts field",
18083
+ {
18084
+ jiraIssueId: external_exports.string().describe("Local JI-xxx ID"),
18085
+ artifactId: external_exports.string().describe("Marvin artifact ID to link (e.g. 'D-001', 'F-003')")
18086
+ },
18087
+ async (args) => {
18088
+ const doc = store.get(args.jiraIssueId);
18089
+ if (!doc || doc.frontmatter.type !== JIRA_TYPE) {
18090
+ return {
18091
+ content: [
18092
+ {
18093
+ type: "text",
18094
+ text: `Jira issue ${args.jiraIssueId} not found locally`
18095
+ }
18096
+ ],
18097
+ isError: true
18098
+ };
18099
+ }
18100
+ const artifact = store.get(args.artifactId);
18101
+ if (!artifact) {
18102
+ return {
18103
+ content: [
18104
+ { type: "text", text: `Artifact ${args.artifactId} not found` }
18105
+ ],
18106
+ isError: true
18107
+ };
18108
+ }
18109
+ const linked = doc.frontmatter.linkedArtifacts ?? [];
18110
+ if (linked.includes(args.artifactId)) {
18111
+ return {
18112
+ content: [
18113
+ {
18114
+ type: "text",
18115
+ text: `${args.artifactId} is already linked to ${args.jiraIssueId}`
18116
+ }
18117
+ ]
18118
+ };
18119
+ }
18120
+ store.update(args.jiraIssueId, {
18121
+ linkedArtifacts: [...linked, args.artifactId]
18122
+ });
18123
+ return {
18124
+ content: [
18125
+ {
18126
+ type: "text",
18127
+ text: `Linked ${args.artifactId} to ${args.jiraIssueId}`
18128
+ }
18129
+ ]
18130
+ };
18131
+ }
18132
+ )
18133
+ ];
18134
+ }
18135
+
18136
+ // src/skills/builtin/jira/index.ts
18137
+ var jiraSkill = {
18138
+ id: "jira",
18139
+ name: "Jira Integration",
18140
+ description: "Bidirectional sync between Marvin artifacts and Jira issues",
18141
+ version: "1.0.0",
18142
+ format: "builtin-ts",
18143
+ // No default persona affinity — opt-in via config.yaml skills section
18144
+ documentTypeRegistrations: [
18145
+ { type: "jira-issue", dirName: "jira-issues", idPrefix: "JI" }
18146
+ ],
18147
+ tools: (store) => createJiraTools(store),
18148
+ promptFragments: {
18149
+ "product-owner": `You have the **Jira Integration** skill. You can pull issues from Jira and push Marvin artifacts to Jira.
18150
+
18151
+ **Available tools:**
18152
+ - \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues
18153
+ - \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import issues from Jira by key or JQL query
18154
+ - \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, feature, etc.)
18155
+ - \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
18156
+ - \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx
18157
+
18158
+ **As Product Owner, use Jira integration to:**
18159
+ - Pull stakeholder-reported issues for triage and prioritization
18160
+ - Push approved features as Stories for development tracking
18161
+ - Link decisions to Jira issues for audit trail and traceability
18162
+ - Use JQL queries to review backlog status (e.g. \`project = PROJ AND status = "To Do"\`)`,
18163
+ "tech-lead": `You have the **Jira Integration** skill. You can pull issues from Jira and push Marvin artifacts to Jira.
18164
+
18165
+ **Available tools:**
18166
+ - \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues
18167
+ - \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import issues from Jira by key or JQL query
18168
+ - \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, action, epic, etc.)
18169
+ - \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
18170
+ - \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx
18171
+
18172
+ **As Tech Lead, use Jira integration to:**
18173
+ - Pull technical issues and bugs for sprint planning and estimation
18174
+ - Push epics and technical decisions to Jira for cross-team visibility
18175
+ - Bidirectional sync to keep local governance and Jira in alignment
18176
+ - Use JQL queries to track technical debt (e.g. \`labels = "tech-debt" AND status != "Done"\`)`,
18177
+ "delivery-manager": `You have the **Jira Integration** skill. You can pull issues from Jira and push Marvin artifacts to Jira.
18178
+
18179
+ **Available tools:**
18180
+ - \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues
18181
+ - \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import issues from Jira by key or JQL query
18182
+ - \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, action, etc.)
18183
+ - \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
18184
+ - \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx
18185
+
18186
+ **As Delivery Manager, use Jira integration to:**
18187
+ - Pull sprint issues for tracking progress and blockers
18188
+ - Push actions and decisions to Jira for stakeholder visibility
18189
+ - Use JQL queries for reporting (e.g. \`sprint in openSprints() AND assignee = currentUser()\`)
18190
+ - Sync status between Marvin governance items and Jira issues`
18191
+ }
18192
+ };
18193
+
17618
18194
  // src/skills/registry.ts
17619
18195
  var BUILTIN_SKILLS = {
17620
- "governance-review": governanceReviewSkill
18196
+ "governance-review": governanceReviewSkill,
18197
+ "jira": jiraSkill
17621
18198
  };
17622
18199
  var GOVERNANCE_TOOL_NAMES = [
17623
18200
  "mcp__marvin-governance__list_decisions",
@@ -17766,6 +18343,16 @@ function resolveSkillsForPersona(personaId, skillsConfig, allSkills) {
17766
18343
  }
17767
18344
  return result;
17768
18345
  }
18346
+ function collectSkillRegistrations(skillIds, allSkills) {
18347
+ const registrations = [];
18348
+ for (const id of skillIds) {
18349
+ const skill = allSkills.get(id);
18350
+ if (skill?.documentTypeRegistrations) {
18351
+ registrations.push(...skill.documentTypeRegistrations);
18352
+ }
18353
+ }
18354
+ return registrations;
18355
+ }
17769
18356
  function getSkillTools(skillIds, allSkills, store) {
17770
18357
  const tools = [];
17771
18358
  for (const id of skillIds) {
@@ -17877,16 +18464,17 @@ ${wildcardPrompt}
17877
18464
  async function startSession(options) {
17878
18465
  const { persona, config: config2, marvinDir, projectRoot } = options;
17879
18466
  const plugin = resolvePlugin(config2.project.methodology);
17880
- const registrations = plugin?.documentTypeRegistrations ?? [];
17881
- const store = new DocumentStore(marvinDir, registrations);
18467
+ const pluginRegistrations = plugin?.documentTypeRegistrations ?? [];
18468
+ const allSkills = loadAllSkills(marvinDir);
18469
+ const skillIds = resolveSkillsForPersona(persona.id, config2.project.skills, allSkills);
18470
+ const skillRegistrations = collectSkillRegistrations(skillIds, allSkills);
18471
+ const store = new DocumentStore(marvinDir, [...pluginRegistrations, ...skillRegistrations]);
17882
18472
  const sessionStore = new SessionStore(marvinDir);
17883
18473
  const sourcesDir = path9.join(marvinDir, "sources");
17884
18474
  const hasSourcesDir = fs9.existsSync(sourcesDir);
17885
18475
  const manifest = hasSourcesDir ? new SourceManifestManager(marvinDir) : void 0;
17886
18476
  const pluginTools = plugin ? getPluginTools(plugin, store, marvinDir) : [];
17887
18477
  const pluginPromptFragment = plugin ? getPluginPromptFragment(plugin, persona.id) : void 0;
17888
- const allSkills = loadAllSkills(marvinDir);
17889
- const skillIds = resolveSkillsForPersona(persona.id, config2.project.skills, allSkills);
17890
18478
  const codeSkillTools = getSkillTools(skillIds, allSkills, store);
17891
18479
  const skillAgents = getSkillAgentDefinitions(skillIds, allSkills);
17892
18480
  const skillPromptFragment = getSkillPromptFragment(skillIds, allSkills, persona.id);
@@ -18976,7 +19564,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
18976
19564
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
18977
19565
 
18978
19566
  // src/skills/action-tools.ts
18979
- import { tool as tool19 } from "@anthropic-ai/claude-agent-sdk";
19567
+ import { tool as tool20 } from "@anthropic-ai/claude-agent-sdk";
18980
19568
 
18981
19569
  // src/skills/action-runner.ts
18982
19570
  import { query as query4 } from "@anthropic-ai/claude-agent-sdk";
@@ -19042,7 +19630,7 @@ function createSkillActionTools(skills, context) {
19042
19630
  if (!skill.actions) continue;
19043
19631
  for (const action of skill.actions) {
19044
19632
  tools.push(
19045
- tool19(
19633
+ tool20(
19046
19634
  `${skill.id}__${action.id}`,
19047
19635
  action.description,
19048
19636
  {
@@ -19134,10 +19722,10 @@ ${lines.join("\n\n")}`;
19134
19722
  }
19135
19723
 
19136
19724
  // src/mcp/persona-tools.ts
19137
- import { tool as tool20 } from "@anthropic-ai/claude-agent-sdk";
19725
+ import { tool as tool21 } from "@anthropic-ai/claude-agent-sdk";
19138
19726
  function createPersonaTools(ctx, marvinDir) {
19139
19727
  return [
19140
- tool20(
19728
+ tool21(
19141
19729
  "set_persona",
19142
19730
  "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.",
19143
19731
  {
@@ -19167,7 +19755,7 @@ ${summaries}`
19167
19755
  };
19168
19756
  }
19169
19757
  ),
19170
- tool20(
19758
+ tool21(
19171
19759
  "get_persona_guidance",
19172
19760
  "Get guidance for a persona without changing the active persona. If no persona is specified, lists all available personas with summaries.",
19173
19761
  {
@@ -19398,20 +19986,23 @@ async function skillsInstallCommand(skillId, options) {
19398
19986
  console.log(chalk10.red("Please specify a persona with --as <persona>."));
19399
19987
  return;
19400
19988
  }
19989
+ const targets = persona === "all" ? listPersonas().map((p) => p.id) : [persona];
19401
19990
  const config2 = loadProjectConfig(project.marvinDir);
19402
19991
  if (!config2.skills) {
19403
19992
  config2.skills = {};
19404
19993
  }
19405
- if (!config2.skills[persona]) {
19406
- config2.skills[persona] = [];
19407
- }
19408
- if (config2.skills[persona].includes(skillId)) {
19409
- console.log(chalk10.yellow(`Skill "${skillId}" is already assigned to ${persona}.`));
19410
- return;
19994
+ for (const target of targets) {
19995
+ if (!config2.skills[target]) {
19996
+ config2.skills[target] = [];
19997
+ }
19998
+ if (config2.skills[target].includes(skillId)) {
19999
+ console.log(chalk10.yellow(`Skill "${skillId}" is already assigned to ${target}.`));
20000
+ continue;
20001
+ }
20002
+ config2.skills[target].push(skillId);
20003
+ console.log(chalk10.green(`Assigned skill "${skillId}" to ${target}.`));
19411
20004
  }
19412
- config2.skills[persona].push(skillId);
19413
20005
  saveProjectConfig(project.marvinDir, config2);
19414
- console.log(chalk10.green(`Assigned skill "${skillId}" to ${persona}.`));
19415
20006
  }
19416
20007
  async function skillsRemoveCommand(skillId, options) {
19417
20008
  const project = loadProject();
@@ -19420,25 +20011,28 @@ async function skillsRemoveCommand(skillId, options) {
19420
20011
  console.log(chalk10.red("Please specify a persona with --as <persona>."));
19421
20012
  return;
19422
20013
  }
20014
+ const targets = persona === "all" ? listPersonas().map((p) => p.id) : [persona];
19423
20015
  const config2 = loadProjectConfig(project.marvinDir);
19424
- if (!config2.skills?.[persona]) {
19425
- console.log(chalk10.yellow(`No skills configured for ${persona}.`));
19426
- return;
19427
- }
19428
- const idx = config2.skills[persona].indexOf(skillId);
19429
- if (idx === -1) {
19430
- console.log(chalk10.yellow(`Skill "${skillId}" is not assigned to ${persona}.`));
19431
- return;
19432
- }
19433
- config2.skills[persona].splice(idx, 1);
19434
- if (config2.skills[persona].length === 0) {
19435
- delete config2.skills[persona];
20016
+ for (const target of targets) {
20017
+ if (!config2.skills?.[target]) {
20018
+ console.log(chalk10.yellow(`No skills configured for ${target}.`));
20019
+ continue;
20020
+ }
20021
+ const idx = config2.skills[target].indexOf(skillId);
20022
+ if (idx === -1) {
20023
+ console.log(chalk10.yellow(`Skill "${skillId}" is not assigned to ${target}.`));
20024
+ continue;
20025
+ }
20026
+ config2.skills[target].splice(idx, 1);
20027
+ if (config2.skills[target].length === 0) {
20028
+ delete config2.skills[target];
20029
+ }
20030
+ console.log(chalk10.green(`Removed skill "${skillId}" from ${target}.`));
19436
20031
  }
19437
- if (Object.keys(config2.skills).length === 0) {
20032
+ if (config2.skills && Object.keys(config2.skills).length === 0) {
19438
20033
  delete config2.skills;
19439
20034
  }
19440
20035
  saveProjectConfig(project.marvinDir, config2);
19441
- console.log(chalk10.green(`Removed skill "${skillId}" from ${persona}.`));
19442
20036
  }
19443
20037
  async function skillsCreateCommand(name) {
19444
20038
  const project = loadProject();
@@ -20685,12 +21279,94 @@ Contribution: ${options.type}`));
20685
21279
  });
20686
21280
  }
20687
21281
 
21282
+ // src/reports/gar/render-ascii.ts
21283
+ import chalk16 from "chalk";
21284
+ var STATUS_DOT = {
21285
+ green: chalk16.green("\u25CF"),
21286
+ amber: chalk16.yellow("\u25CF"),
21287
+ red: chalk16.red("\u25CF")
21288
+ };
21289
+ var STATUS_LABEL = {
21290
+ green: chalk16.green.bold("GREEN"),
21291
+ amber: chalk16.yellow.bold("AMBER"),
21292
+ red: chalk16.red.bold("RED")
21293
+ };
21294
+ var SEPARATOR = chalk16.dim("\u2500".repeat(60));
21295
+ function renderAscii(report) {
21296
+ const lines = [];
21297
+ lines.push("");
21298
+ lines.push(chalk16.bold(` GAR Report \xB7 ${report.projectName}`));
21299
+ lines.push(chalk16.dim(` ${report.generatedAt}`));
21300
+ lines.push("");
21301
+ lines.push(` Overall: ${STATUS_LABEL[report.overall]}`);
21302
+ lines.push("");
21303
+ lines.push(` ${SEPARATOR}`);
21304
+ for (const area of report.areas) {
21305
+ lines.push(` ${STATUS_DOT[area.status]} ${chalk16.bold(area.name.padEnd(12))} ${area.summary}`);
21306
+ for (const item of area.items) {
21307
+ lines.push(` ${chalk16.dim("\u2514")} ${item.id} ${item.title}`);
21308
+ }
21309
+ }
21310
+ lines.push(` ${SEPARATOR}`);
21311
+ lines.push("");
21312
+ return lines.join("\n");
21313
+ }
21314
+
21315
+ // src/reports/gar/render-confluence.ts
21316
+ var EMOJI = {
21317
+ green: ":green_circle:",
21318
+ amber: ":yellow_circle:",
21319
+ red: ":red_circle:"
21320
+ };
21321
+ function renderConfluence(report) {
21322
+ const lines = [];
21323
+ lines.push(`# GAR Report \u2014 ${report.projectName}`);
21324
+ lines.push("");
21325
+ lines.push(`**Date:** ${report.generatedAt}`);
21326
+ lines.push(`**Overall:** ${EMOJI[report.overall]} ${report.overall.toUpperCase()}`);
21327
+ lines.push("");
21328
+ lines.push("| Area | Status | Summary |");
21329
+ lines.push("|------|--------|---------|");
21330
+ for (const area of report.areas) {
21331
+ lines.push(
21332
+ `| ${area.name} | ${EMOJI[area.status]} ${area.status.toUpperCase()} | ${area.summary} |`
21333
+ );
21334
+ }
21335
+ lines.push("");
21336
+ for (const area of report.areas) {
21337
+ if (area.items.length === 0) continue;
21338
+ lines.push(`## ${area.name}`);
21339
+ lines.push("");
21340
+ for (const item of area.items) {
21341
+ lines.push(`- **${item.id}** ${item.title}`);
21342
+ }
21343
+ lines.push("");
21344
+ }
21345
+ return lines.join("\n");
21346
+ }
21347
+
21348
+ // src/cli/commands/report.ts
21349
+ async function garReportCommand(options) {
21350
+ const project = loadProject();
21351
+ const plugin = resolvePlugin(project.config.methodology);
21352
+ const registrations = plugin?.documentTypeRegistrations ?? [];
21353
+ const store = new DocumentStore(project.marvinDir, registrations);
21354
+ const metrics = collectGarMetrics(store);
21355
+ const report = evaluateGar(project.config.name, metrics);
21356
+ const format = options.format ?? "ascii";
21357
+ if (format === "confluence") {
21358
+ console.log(renderConfluence(report));
21359
+ } else {
21360
+ console.log(renderAscii(report));
21361
+ }
21362
+ }
21363
+
20688
21364
  // src/cli/program.ts
20689
21365
  function createProgram() {
20690
21366
  const program2 = new Command();
20691
21367
  program2.name("marvin").description(
20692
21368
  "AI-powered product development assistant with Product Owner, Delivery Manager, and Technical Lead personas"
20693
- ).version("0.2.6");
21369
+ ).version("0.2.8");
20694
21370
  program2.command("init").description("Initialize a new Marvin project in the current directory").action(async () => {
20695
21371
  await initCommand();
20696
21372
  });
@@ -20760,6 +21436,13 @@ function createProgram() {
20760
21436
  skillsCmd.command("migrate").description("Migrate YAML skill files to SKILL.md directory format").action(async () => {
20761
21437
  await skillsMigrateCommand();
20762
21438
  });
21439
+ const reportCmd = program2.command("report").description("Generate project reports");
21440
+ reportCmd.command("gar").description("Generate a Green/Amber/Red status report").option(
21441
+ "--format <format>",
21442
+ "Output format: ascii or confluence (default: ascii)"
21443
+ ).action(async (options) => {
21444
+ await garReportCommand(options);
21445
+ });
20763
21446
  return program2;
20764
21447
  }
20765
21448