mrvn-cli 0.5.7 → 0.5.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -19392,7 +19392,7 @@ function poBacklogPage(ctx) {
19392
19392
  }
19393
19393
  }
19394
19394
  }
19395
- const DONE_STATUSES14 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
19395
+ const DONE_STATUSES16 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
19396
19396
  function featureTaskStats(featureId) {
19397
19397
  const fEpics = featureToEpics.get(featureId) ?? [];
19398
19398
  let total = 0;
@@ -19401,7 +19401,7 @@ function poBacklogPage(ctx) {
19401
19401
  for (const epic of fEpics) {
19402
19402
  for (const t of epicToTasks.get(epic.frontmatter.id) ?? []) {
19403
19403
  total++;
19404
- if (DONE_STATUSES14.has(t.frontmatter.status)) done++;
19404
+ if (DONE_STATUSES16.has(t.frontmatter.status)) done++;
19405
19405
  progressSum += getEffectiveProgress(t.frontmatter);
19406
19406
  }
19407
19407
  }
@@ -24895,13 +24895,23 @@ import { tool as tool20 } from "@anthropic-ai/claude-agent-sdk";
24895
24895
  // src/skills/builtin/jira/client.ts
24896
24896
  var JiraClient = class {
24897
24897
  baseUrl;
24898
+ baseUrlV3;
24898
24899
  authHeader;
24899
24900
  constructor(config2) {
24900
- this.baseUrl = `https://${config2.host}/rest/api/2`;
24901
+ const host = config2.host.replace(/^https?:\/\//, "").replace(/\/+$/, "");
24902
+ this.baseUrl = `https://${host}/rest/api/2`;
24903
+ this.baseUrlV3 = `https://${host}/rest/api/3`;
24901
24904
  this.authHeader = "Basic " + Buffer.from(`${config2.email}:${config2.apiToken}`).toString("base64");
24902
24905
  }
24903
24906
  async request(path21, method = "GET", body) {
24904
24907
  const url2 = `${this.baseUrl}${path21}`;
24908
+ return this.doRequest(url2, method, body);
24909
+ }
24910
+ async requestV3(path21, method = "GET", body) {
24911
+ const url2 = `${this.baseUrlV3}${path21}`;
24912
+ return this.doRequest(url2, method, body);
24913
+ }
24914
+ async doRequest(url2, method, body) {
24905
24915
  const headers = {
24906
24916
  Authorization: this.authHeader,
24907
24917
  "Content-Type": "application/json",
@@ -24915,18 +24925,26 @@ var JiraClient = class {
24915
24925
  if (!response.ok) {
24916
24926
  const text = await response.text().catch(() => "");
24917
24927
  throw new Error(
24918
- `Jira API error ${response.status} ${method} ${path21}: ${text}`
24928
+ `Jira API error ${response.status} ${method} ${url2}: ${text}`
24919
24929
  );
24920
24930
  }
24921
24931
  if (response.status === 204) return void 0;
24922
24932
  return response.json();
24923
24933
  }
24924
24934
  async searchIssues(jql, maxResults = 50) {
24935
+ return this.searchIssuesV3(
24936
+ jql,
24937
+ ["summary", "description", "status", "issuetype", "priority", "assignee", "labels", "created", "updated"],
24938
+ maxResults
24939
+ );
24940
+ }
24941
+ async searchIssuesV3(jql, fields = ["summary", "status", "issuetype", "priority", "assignee", "labels"], maxResults = 50) {
24925
24942
  const params = new URLSearchParams({
24926
24943
  jql,
24927
- maxResults: String(maxResults)
24944
+ maxResults: String(maxResults),
24945
+ fields: fields.join(",")
24928
24946
  });
24929
- return this.request(`/search?${params}`);
24947
+ return this.requestV3(`/search/jql?${params}`);
24930
24948
  }
24931
24949
  async getIssue(key) {
24932
24950
  return this.request(`/issue/${encodeURIComponent(key)}`);
@@ -24941,6 +24959,28 @@ var JiraClient = class {
24941
24959
  { fields }
24942
24960
  );
24943
24961
  }
24962
+ async getIssueWithLinks(key) {
24963
+ return this.request(
24964
+ `/issue/${encodeURIComponent(key)}?fields=summary,status,issuetype,priority,assignee,labels,subtasks,issuelinks`
24965
+ );
24966
+ }
24967
+ async getChangelog(key) {
24968
+ const result = await this.request(
24969
+ `/issue/${encodeURIComponent(key)}/changelog?maxResults=100`
24970
+ );
24971
+ return result.values;
24972
+ }
24973
+ async getComments(key) {
24974
+ const result = await this.request(
24975
+ `/issue/${encodeURIComponent(key)}/comment?maxResults=100`
24976
+ );
24977
+ return result.comments;
24978
+ }
24979
+ async getRemoteLinks(key) {
24980
+ return this.request(
24981
+ `/issue/${encodeURIComponent(key)}/remotelink`
24982
+ );
24983
+ }
24944
24984
  async addComment(key, body) {
24945
24985
  await this.request(
24946
24986
  `/issue/${encodeURIComponent(key)}/comment`,
@@ -24954,7 +24994,651 @@ function createJiraClient(jiraUserConfig) {
24954
24994
  const email3 = jiraUserConfig?.email ?? process.env.JIRA_EMAIL;
24955
24995
  const apiToken = jiraUserConfig?.apiToken ?? process.env.JIRA_API_TOKEN;
24956
24996
  if (!host || !email3 || !apiToken) return null;
24957
- return { client: new JiraClient({ host, email: email3, apiToken }), host };
24997
+ const normalizedHost = host.replace(/^https?:\/\//, "").replace(/\/+$/, "");
24998
+ return { client: new JiraClient({ host, email: email3, apiToken }), host: normalizedHost };
24999
+ }
25000
+
25001
+ // src/skills/builtin/jira/sync.ts
25002
+ var DONE_STATUSES14 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "obsolete", "wont do"]);
25003
+ var DEFAULT_ACTION_STATUS_MAP = {
25004
+ done: ["Done", "Closed", "Resolved", "Obsolete", "Wont Do"],
25005
+ "in-progress": ["In Progress", "In Review", "Reviewing", "Testing"],
25006
+ blocked: ["Blocked"],
25007
+ open: ["To Do", "Open", "Backlog", "New"]
25008
+ };
25009
+ var DEFAULT_TASK_STATUS_MAP = {
25010
+ done: ["Done", "Closed", "Resolved", "Obsolete", "Wont Do"],
25011
+ review: ["In Review", "Code Review", "Reviewing", "Testing"],
25012
+ "in-progress": ["In Progress"],
25013
+ ready: ["Ready", "Selected for Development"],
25014
+ blocked: ["Blocked"],
25015
+ backlog: ["To Do", "Open", "Backlog", "New"]
25016
+ };
25017
+ function buildStatusLookup(configMap, defaults) {
25018
+ const map2 = configMap ?? defaults;
25019
+ const lookup = /* @__PURE__ */ new Map();
25020
+ for (const [marvinStatus, jiraStatuses] of Object.entries(map2)) {
25021
+ for (const js of jiraStatuses) {
25022
+ lookup.set(js.toLowerCase(), marvinStatus);
25023
+ }
25024
+ }
25025
+ return lookup;
25026
+ }
25027
+ function mapJiraStatusForAction(status, configMap) {
25028
+ const lookup = buildStatusLookup(configMap, DEFAULT_ACTION_STATUS_MAP);
25029
+ return lookup.get(status.toLowerCase()) ?? "open";
25030
+ }
25031
+ function mapJiraStatusForTask(status, configMap) {
25032
+ const lookup = buildStatusLookup(configMap, DEFAULT_TASK_STATUS_MAP);
25033
+ return lookup.get(status.toLowerCase()) ?? "backlog";
25034
+ }
25035
+ function computeSubtaskProgress(subtasks) {
25036
+ if (subtasks.length === 0) return 0;
25037
+ const done = subtasks.filter(
25038
+ (s) => DONE_STATUSES14.has(s.fields.status.name.toLowerCase())
25039
+ ).length;
25040
+ return Math.round(done / subtasks.length * 100);
25041
+ }
25042
+ async function fetchJiraStatus(store, client, host, artifactId, statusMap) {
25043
+ const result = { artifacts: [], errors: [] };
25044
+ const actions = store.list({ type: "action" });
25045
+ const tasks = store.list({ type: "task" });
25046
+ let candidates = [...actions, ...tasks].filter(
25047
+ (d) => d.frontmatter.jiraKey
25048
+ );
25049
+ if (artifactId) {
25050
+ candidates = candidates.filter((d) => d.frontmatter.id === artifactId);
25051
+ if (candidates.length === 0) {
25052
+ const doc = store.get(artifactId);
25053
+ if (doc) {
25054
+ result.errors.push(
25055
+ `${artifactId} has no jiraKey \u2014 use push_artifact_to_jira or link_to_jira first`
25056
+ );
25057
+ } else {
25058
+ result.errors.push(`Artifact ${artifactId} not found`);
25059
+ }
25060
+ return result;
25061
+ }
25062
+ }
25063
+ candidates = candidates.filter(
25064
+ (d) => !DONE_STATUSES14.has(d.frontmatter.status)
25065
+ );
25066
+ for (const doc of candidates) {
25067
+ const jiraKey = doc.frontmatter.jiraKey;
25068
+ const artifactType = doc.frontmatter.type;
25069
+ try {
25070
+ const issue2 = await client.getIssueWithLinks(jiraKey);
25071
+ const proposedStatus = artifactType === "task" ? mapJiraStatusForTask(issue2.fields.status.name, statusMap?.task) : mapJiraStatusForAction(issue2.fields.status.name, statusMap?.action);
25072
+ const currentStatus = doc.frontmatter.status;
25073
+ const linkedIssues = [];
25074
+ if (issue2.fields.subtasks) {
25075
+ for (const sub of issue2.fields.subtasks) {
25076
+ linkedIssues.push({
25077
+ key: sub.key,
25078
+ summary: sub.fields.summary,
25079
+ status: sub.fields.status.name,
25080
+ relationship: "subtask",
25081
+ isDone: DONE_STATUSES14.has(sub.fields.status.name.toLowerCase())
25082
+ });
25083
+ }
25084
+ }
25085
+ if (issue2.fields.issuelinks) {
25086
+ for (const link of issue2.fields.issuelinks) {
25087
+ if (link.outwardIssue) {
25088
+ linkedIssues.push({
25089
+ key: link.outwardIssue.key,
25090
+ summary: link.outwardIssue.fields.summary,
25091
+ status: link.outwardIssue.fields.status.name,
25092
+ relationship: link.type.outward,
25093
+ isDone: DONE_STATUSES14.has(
25094
+ link.outwardIssue.fields.status.name.toLowerCase()
25095
+ )
25096
+ });
25097
+ }
25098
+ if (link.inwardIssue) {
25099
+ linkedIssues.push({
25100
+ key: link.inwardIssue.key,
25101
+ summary: link.inwardIssue.fields.summary,
25102
+ status: link.inwardIssue.fields.status.name,
25103
+ relationship: link.type.inward,
25104
+ isDone: DONE_STATUSES14.has(
25105
+ link.inwardIssue.fields.status.name.toLowerCase()
25106
+ )
25107
+ });
25108
+ }
25109
+ }
25110
+ }
25111
+ const subtasks = issue2.fields.subtasks ?? [];
25112
+ let proposedProgress;
25113
+ if (subtasks.length > 0 && !doc.frontmatter.progressOverride) {
25114
+ proposedProgress = computeSubtaskProgress(subtasks);
25115
+ }
25116
+ const currentProgress = doc.frontmatter.progress;
25117
+ result.artifacts.push({
25118
+ id: doc.frontmatter.id,
25119
+ type: artifactType,
25120
+ jiraKey,
25121
+ jiraUrl: `https://${host}/browse/${jiraKey}`,
25122
+ jiraSummary: issue2.fields.summary,
25123
+ jiraStatus: issue2.fields.status.name,
25124
+ currentMarvinStatus: currentStatus,
25125
+ proposedMarvinStatus: proposedStatus,
25126
+ statusChanged: currentStatus !== proposedStatus,
25127
+ currentProgress,
25128
+ proposedProgress,
25129
+ progressChanged: proposedProgress !== void 0 && proposedProgress !== currentProgress,
25130
+ linkedIssues
25131
+ });
25132
+ } catch (err) {
25133
+ result.errors.push(
25134
+ `${doc.frontmatter.id} (${jiraKey}): ${err instanceof Error ? err.message : String(err)}`
25135
+ );
25136
+ }
25137
+ }
25138
+ return result;
25139
+ }
25140
+ async function syncJiraProgress(store, client, host, artifactId, statusMap) {
25141
+ const fetchResult = await fetchJiraStatus(store, client, host, artifactId, statusMap);
25142
+ const result = {
25143
+ updated: [],
25144
+ unchanged: 0,
25145
+ errors: [...fetchResult.errors]
25146
+ };
25147
+ for (const artifact of fetchResult.artifacts) {
25148
+ const hasChanges = artifact.statusChanged || artifact.progressChanged || artifact.linkedIssues.length > 0;
25149
+ if (hasChanges) {
25150
+ const updates = {
25151
+ status: artifact.proposedMarvinStatus,
25152
+ lastJiraSyncAt: (/* @__PURE__ */ new Date()).toISOString(),
25153
+ jiraLinkedIssues: artifact.linkedIssues
25154
+ };
25155
+ if (artifact.proposedProgress !== void 0) {
25156
+ updates.progress = artifact.proposedProgress;
25157
+ }
25158
+ store.update(artifact.id, updates);
25159
+ if (artifact.type === "task") {
25160
+ propagateProgressFromTask(store, artifact.id);
25161
+ } else if (artifact.type === "action") {
25162
+ propagateProgressToAction(store, artifact.id);
25163
+ }
25164
+ result.updated.push({
25165
+ id: artifact.id,
25166
+ jiraKey: artifact.jiraKey,
25167
+ oldStatus: artifact.currentMarvinStatus,
25168
+ newStatus: artifact.proposedMarvinStatus,
25169
+ linkedIssues: artifact.linkedIssues
25170
+ });
25171
+ } else {
25172
+ store.update(artifact.id, {
25173
+ lastJiraSyncAt: (/* @__PURE__ */ new Date()).toISOString()
25174
+ });
25175
+ result.unchanged++;
25176
+ }
25177
+ }
25178
+ return result;
25179
+ }
25180
+
25181
+ // src/skills/builtin/jira/daily.ts
25182
+ var BLOCKER_PATTERNS = [
25183
+ /\bblocked\b/i,
25184
+ /\bblocking\b/i,
25185
+ /\bwaiting\s+for\b/i,
25186
+ /\bon\s+hold\b/i,
25187
+ /\bcan'?t\s+proceed\b/i,
25188
+ /\bdepends?\s+on\b/i,
25189
+ /\bstuck\b/i,
25190
+ /\bneed[s]?\s+(to\s+wait|approval|input|clarification)\b/i
25191
+ ];
25192
+ var DECISION_PATTERNS = [
25193
+ /\bdecided\b/i,
25194
+ /\bagreed\b/i,
25195
+ /\bapproved?\b/i,
25196
+ /\blet'?s?\s+go\s+with\b/i,
25197
+ /\bwe('ll|\s+will)\s+(use|go|proceed|adopt)\b/i,
25198
+ /\bsigned\s+off\b/i,
25199
+ /\bconfirmed\b/i
25200
+ ];
25201
+ var QUESTION_PATTERNS = [
25202
+ /\?/,
25203
+ /\bdoes\s+anyone\s+know\b/i,
25204
+ /\bhow\s+should\s+we\b/i,
25205
+ /\bneed\s+clarification\b/i,
25206
+ /\bwhat('s|\s+is)\s+the\s+(plan|approach|status)\b/i,
25207
+ /\bshould\s+we\b/i,
25208
+ /\bany\s+(idea|thought|suggestion)s?\b/i,
25209
+ /\bopen\s+question\b/i
25210
+ ];
25211
+ var RESOLUTION_PATTERNS = [
25212
+ /\bfixed\b/i,
25213
+ /\bresolved\b/i,
25214
+ /\bmerged\b/i,
25215
+ /\bdeployed\b/i,
25216
+ /\bcompleted?\b/i,
25217
+ /\bshipped\b/i,
25218
+ /\bimplemented\b/i,
25219
+ /\bclosed\b/i
25220
+ ];
25221
+ function detectCommentSignals(text) {
25222
+ const signals = [];
25223
+ const lines = text.split("\n");
25224
+ for (const line of lines) {
25225
+ const trimmed = line.trim();
25226
+ if (!trimmed) continue;
25227
+ for (const pattern of BLOCKER_PATTERNS) {
25228
+ if (pattern.test(trimmed)) {
25229
+ signals.push({ type: "blocker", snippet: truncate(trimmed, 120) });
25230
+ break;
25231
+ }
25232
+ }
25233
+ for (const pattern of DECISION_PATTERNS) {
25234
+ if (pattern.test(trimmed)) {
25235
+ signals.push({ type: "decision", snippet: truncate(trimmed, 120) });
25236
+ break;
25237
+ }
25238
+ }
25239
+ for (const pattern of QUESTION_PATTERNS) {
25240
+ if (pattern.test(trimmed)) {
25241
+ signals.push({ type: "question", snippet: truncate(trimmed, 120) });
25242
+ break;
25243
+ }
25244
+ }
25245
+ for (const pattern of RESOLUTION_PATTERNS) {
25246
+ if (pattern.test(trimmed)) {
25247
+ signals.push({ type: "resolution", snippet: truncate(trimmed, 120) });
25248
+ break;
25249
+ }
25250
+ }
25251
+ }
25252
+ const seen = /* @__PURE__ */ new Set();
25253
+ return signals.filter((s) => {
25254
+ if (seen.has(s.type)) return false;
25255
+ seen.add(s.type);
25256
+ return true;
25257
+ });
25258
+ }
25259
+ var STOP_WORDS = /* @__PURE__ */ new Set([
25260
+ "a",
25261
+ "an",
25262
+ "the",
25263
+ "and",
25264
+ "or",
25265
+ "but",
25266
+ "in",
25267
+ "on",
25268
+ "at",
25269
+ "to",
25270
+ "for",
25271
+ "of",
25272
+ "with",
25273
+ "by",
25274
+ "from",
25275
+ "is",
25276
+ "are",
25277
+ "was",
25278
+ "were",
25279
+ "be",
25280
+ "been",
25281
+ "this",
25282
+ "that",
25283
+ "it",
25284
+ "its",
25285
+ "as",
25286
+ "not",
25287
+ "no",
25288
+ "if",
25289
+ "do",
25290
+ "does",
25291
+ "new",
25292
+ "via",
25293
+ "use",
25294
+ "using",
25295
+ "based",
25296
+ "into",
25297
+ "e.g",
25298
+ "etc"
25299
+ ]);
25300
+ function tokenize(text) {
25301
+ return new Set(
25302
+ text.toLowerCase().replace(/[^a-z0-9\s-]/g, " ").split(/[\s-]+/).filter((w) => w.length > 2 && !STOP_WORDS.has(w))
25303
+ );
25304
+ }
25305
+ function computeTitleSimilarity(jiraSummary, artifactTitle) {
25306
+ const jiraTokens = tokenize(jiraSummary);
25307
+ const artifactTokens = tokenize(artifactTitle);
25308
+ if (jiraTokens.size === 0 || artifactTokens.size === 0) {
25309
+ return { score: 0, sharedTerms: [] };
25310
+ }
25311
+ const shared = [];
25312
+ for (const token of jiraTokens) {
25313
+ if (artifactTokens.has(token)) {
25314
+ shared.push(token);
25315
+ }
25316
+ }
25317
+ const union2 = /* @__PURE__ */ new Set([...jiraTokens, ...artifactTokens]);
25318
+ const score = shared.length / union2.size;
25319
+ return { score, sharedTerms: shared };
25320
+ }
25321
+ var LINK_SUGGESTION_THRESHOLD = 0.15;
25322
+ var MAX_LINK_SUGGESTIONS = 3;
25323
+ function findLinkSuggestions(jiraSummary, allDocs) {
25324
+ const suggestions = [];
25325
+ for (const doc of allDocs) {
25326
+ const fm = doc.frontmatter;
25327
+ if (fm.jiraKey) continue;
25328
+ const { score, sharedTerms } = computeTitleSimilarity(
25329
+ jiraSummary,
25330
+ fm.title
25331
+ );
25332
+ if (score >= LINK_SUGGESTION_THRESHOLD && sharedTerms.length >= 2) {
25333
+ suggestions.push({
25334
+ artifactId: fm.id,
25335
+ artifactType: fm.type,
25336
+ artifactTitle: fm.title,
25337
+ score,
25338
+ sharedTerms
25339
+ });
25340
+ }
25341
+ }
25342
+ return suggestions.sort((a, b) => b.score - a.score).slice(0, MAX_LINK_SUGGESTIONS);
25343
+ }
25344
+ function extractCommentText(body) {
25345
+ if (typeof body === "string") return body;
25346
+ if (!body || typeof body !== "object") return "";
25347
+ const parts = [];
25348
+ function walk(node) {
25349
+ if (!node || typeof node !== "object") return;
25350
+ const n = node;
25351
+ if (n.type === "text" && typeof n.text === "string") {
25352
+ parts.push(n.text);
25353
+ }
25354
+ if (Array.isArray(n.content)) {
25355
+ for (const child of n.content) walk(child);
25356
+ }
25357
+ }
25358
+ walk(body);
25359
+ return parts.join(" ");
25360
+ }
25361
+ function truncate(text, maxLen = 200) {
25362
+ if (text.length <= maxLen) return text;
25363
+ return text.slice(0, maxLen) + "\u2026";
25364
+ }
25365
+ function isWithinRange(timestamp, range) {
25366
+ const date5 = timestamp.slice(0, 10);
25367
+ return date5 >= range.from && date5 <= range.to;
25368
+ }
25369
+ function isConfluenceUrl(url2) {
25370
+ return /atlassian\.net\/wiki\//i.test(url2) || /\/confluence\//i.test(url2);
25371
+ }
25372
+ var DONE_STATUSES15 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "obsolete", "wont do"]);
25373
+ async function fetchJiraDaily(store, client, host, projectKey, dateRange, statusMap) {
25374
+ const summary = {
25375
+ dateRange,
25376
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
25377
+ projectKey,
25378
+ issues: [],
25379
+ proposedActions: [],
25380
+ errors: []
25381
+ };
25382
+ const jql = `project = ${projectKey} AND updated >= "${dateRange.from}" AND updated <= "${dateRange.to} 23:59" ORDER BY updated DESC`;
25383
+ let searchResult;
25384
+ try {
25385
+ searchResult = await client.searchIssuesV3(
25386
+ jql,
25387
+ ["summary", "status", "issuetype", "priority", "assignee", "labels"],
25388
+ 100
25389
+ );
25390
+ } catch (err) {
25391
+ summary.errors.push(
25392
+ `Search failed: ${err instanceof Error ? err.message : String(err)}`
25393
+ );
25394
+ return summary;
25395
+ }
25396
+ const allDocs = [
25397
+ ...store.list({ type: "action" }),
25398
+ ...store.list({ type: "task" }),
25399
+ ...store.list({ type: "decision" }),
25400
+ ...store.list({ type: "question" })
25401
+ ];
25402
+ const otherTypes = store.registeredTypes.filter(
25403
+ (t) => !["action", "task", "decision", "question"].includes(t)
25404
+ );
25405
+ for (const t of otherTypes) {
25406
+ allDocs.push(...store.list({ type: t }));
25407
+ }
25408
+ const jiraKeyToArtifacts = /* @__PURE__ */ new Map();
25409
+ for (const doc of allDocs) {
25410
+ const jk = doc.frontmatter.jiraKey;
25411
+ if (jk) {
25412
+ const list = jiraKeyToArtifacts.get(jk) ?? [];
25413
+ list.push(doc);
25414
+ jiraKeyToArtifacts.set(jk, list);
25415
+ }
25416
+ }
25417
+ const BATCH_SIZE = 5;
25418
+ const issues = searchResult.issues;
25419
+ for (let i = 0; i < issues.length; i += BATCH_SIZE) {
25420
+ const batch = issues.slice(i, i + BATCH_SIZE);
25421
+ const results = await Promise.allSettled(
25422
+ batch.map(
25423
+ (issue2) => processIssue(issue2, client, host, dateRange, jiraKeyToArtifacts, allDocs, statusMap)
25424
+ )
25425
+ );
25426
+ for (let j = 0; j < results.length; j++) {
25427
+ const r = results[j];
25428
+ if (r.status === "fulfilled") {
25429
+ summary.issues.push(r.value);
25430
+ } else {
25431
+ summary.errors.push(
25432
+ `${batch[j].key}: ${r.reason instanceof Error ? r.reason.message : String(r.reason)}`
25433
+ );
25434
+ }
25435
+ }
25436
+ }
25437
+ summary.proposedActions = generateProposedActions(summary.issues);
25438
+ return summary;
25439
+ }
25440
+ async function processIssue(issue2, client, host, dateRange, jiraKeyToArtifacts, allDocs, statusMap) {
25441
+ const [changelogResult, commentsResult, remoteLinksResult, issueWithLinks] = await Promise.all([
25442
+ client.getChangelog(issue2.key).catch(() => []),
25443
+ client.getComments(issue2.key).catch(() => []),
25444
+ client.getRemoteLinks(issue2.key).catch(() => []),
25445
+ client.getIssueWithLinks(issue2.key).catch(() => null)
25446
+ ]);
25447
+ const changes = [];
25448
+ for (const entry of changelogResult) {
25449
+ if (!isWithinRange(entry.created, dateRange)) continue;
25450
+ for (const item of entry.items) {
25451
+ changes.push({
25452
+ field: item.field,
25453
+ from: item.fromString,
25454
+ to: item.toString,
25455
+ author: entry.author.displayName,
25456
+ timestamp: entry.created
25457
+ });
25458
+ }
25459
+ }
25460
+ const comments = [];
25461
+ for (const comment of commentsResult) {
25462
+ if (!isWithinRange(comment.created, dateRange) && !isWithinRange(comment.updated, dateRange)) {
25463
+ continue;
25464
+ }
25465
+ const fullText = extractCommentText(comment.body);
25466
+ const signals = detectCommentSignals(fullText);
25467
+ comments.push({
25468
+ author: comment.author.displayName,
25469
+ created: comment.created,
25470
+ bodyPreview: truncate(fullText),
25471
+ signals
25472
+ });
25473
+ }
25474
+ const confluenceLinks = [];
25475
+ for (const rl of remoteLinksResult) {
25476
+ if (isConfluenceUrl(rl.object.url)) {
25477
+ confluenceLinks.push({
25478
+ url: rl.object.url,
25479
+ title: rl.object.title
25480
+ });
25481
+ }
25482
+ }
25483
+ const linkedIssues = [];
25484
+ if (issueWithLinks) {
25485
+ if (issueWithLinks.fields.subtasks) {
25486
+ for (const sub of issueWithLinks.fields.subtasks) {
25487
+ linkedIssues.push({
25488
+ key: sub.key,
25489
+ summary: sub.fields.summary,
25490
+ status: sub.fields.status.name,
25491
+ relationship: "subtask",
25492
+ isDone: DONE_STATUSES15.has(sub.fields.status.name.toLowerCase())
25493
+ });
25494
+ }
25495
+ }
25496
+ if (issueWithLinks.fields.issuelinks) {
25497
+ for (const link of issueWithLinks.fields.issuelinks) {
25498
+ if (link.outwardIssue) {
25499
+ linkedIssues.push({
25500
+ key: link.outwardIssue.key,
25501
+ summary: link.outwardIssue.fields.summary,
25502
+ status: link.outwardIssue.fields.status.name,
25503
+ relationship: link.type.outward,
25504
+ isDone: DONE_STATUSES15.has(link.outwardIssue.fields.status.name.toLowerCase())
25505
+ });
25506
+ }
25507
+ if (link.inwardIssue) {
25508
+ linkedIssues.push({
25509
+ key: link.inwardIssue.key,
25510
+ summary: link.inwardIssue.fields.summary,
25511
+ status: link.inwardIssue.fields.status.name,
25512
+ relationship: link.type.inward,
25513
+ isDone: DONE_STATUSES15.has(link.inwardIssue.fields.status.name.toLowerCase())
25514
+ });
25515
+ }
25516
+ }
25517
+ }
25518
+ }
25519
+ const marvinArtifacts = [];
25520
+ const artifacts = jiraKeyToArtifacts.get(issue2.key) ?? [];
25521
+ for (const doc of artifacts) {
25522
+ const fm = doc.frontmatter;
25523
+ const artifactType = fm.type;
25524
+ let proposedStatus = null;
25525
+ if (artifactType === "action" || artifactType === "task") {
25526
+ const jiraStatus = issue2.fields.status?.name;
25527
+ if (jiraStatus) {
25528
+ proposedStatus = artifactType === "task" ? mapJiraStatusForTask(jiraStatus, statusMap?.task) : mapJiraStatusForAction(jiraStatus, statusMap?.action);
25529
+ }
25530
+ }
25531
+ marvinArtifacts.push({
25532
+ id: fm.id,
25533
+ type: artifactType,
25534
+ title: fm.title,
25535
+ currentStatus: fm.status,
25536
+ proposedStatus,
25537
+ statusDrift: proposedStatus !== null && proposedStatus !== fm.status
25538
+ });
25539
+ }
25540
+ const linkSuggestions = marvinArtifacts.length === 0 ? findLinkSuggestions(issue2.fields.summary, allDocs) : [];
25541
+ return {
25542
+ key: issue2.key,
25543
+ summary: issue2.fields.summary,
25544
+ currentStatus: issue2.fields.status?.name ?? "Unknown",
25545
+ issueType: issue2.fields.issuetype?.name ?? "Unknown",
25546
+ assignee: issue2.fields.assignee?.displayName ?? null,
25547
+ changes,
25548
+ comments,
25549
+ linkedIssues,
25550
+ confluenceLinks,
25551
+ marvinArtifacts,
25552
+ linkSuggestions
25553
+ };
25554
+ }
25555
+ function generateProposedActions(issues) {
25556
+ const actions = [];
25557
+ for (const issue2 of issues) {
25558
+ for (const artifact of issue2.marvinArtifacts) {
25559
+ if (artifact.statusDrift && artifact.proposedStatus) {
25560
+ actions.push({
25561
+ type: "status-update",
25562
+ description: `Update ${artifact.id} (${artifact.type}) status: ${artifact.currentStatus} \u2192 ${artifact.proposedStatus} (Jira ${issue2.key} is "${issue2.currentStatus}")`,
25563
+ artifactId: artifact.id,
25564
+ jiraKey: issue2.key
25565
+ });
25566
+ }
25567
+ }
25568
+ if (issue2.marvinArtifacts.length === 0 && (issue2.changes.length > 0 || issue2.comments.length > 0)) {
25569
+ actions.push({
25570
+ type: "unlinked-issue",
25571
+ description: `${issue2.key} ("${issue2.summary}") has activity but no Marvin artifact \u2014 consider linking or creating one`,
25572
+ jiraKey: issue2.key
25573
+ });
25574
+ }
25575
+ for (const suggestion of issue2.linkSuggestions) {
25576
+ actions.push({
25577
+ type: "link-suggestion",
25578
+ description: `${issue2.key} ("${issue2.summary}") may match ${suggestion.artifactId} ("${suggestion.artifactTitle}") \u2014 shared terms: ${suggestion.sharedTerms.join(", ")} (${Math.round(suggestion.score * 100)}% similarity)`,
25579
+ artifactId: suggestion.artifactId,
25580
+ jiraKey: issue2.key
25581
+ });
25582
+ }
25583
+ for (const comment of issue2.comments) {
25584
+ for (const signal of comment.signals) {
25585
+ if (signal.type === "blocker") {
25586
+ actions.push({
25587
+ type: "blocker-detected",
25588
+ description: `Blocker in ${issue2.key} comment by ${comment.author}: "${signal.snippet}"`,
25589
+ jiraKey: issue2.key
25590
+ });
25591
+ }
25592
+ if (signal.type === "decision") {
25593
+ actions.push({
25594
+ type: "decision-candidate",
25595
+ description: `Possible decision in ${issue2.key} comment by ${comment.author}: "${signal.snippet}" \u2014 consider creating a decision artifact`,
25596
+ jiraKey: issue2.key
25597
+ });
25598
+ }
25599
+ if (signal.type === "question") {
25600
+ const linkedQuestion = issue2.marvinArtifacts.find(
25601
+ (a) => a.type === "question" && a.currentStatus !== "answered"
25602
+ );
25603
+ if (linkedQuestion) {
25604
+ actions.push({
25605
+ type: "question-candidate",
25606
+ description: `Question in ${issue2.key} comment by ${comment.author} \u2014 may relate to ${linkedQuestion.id} ("${linkedQuestion.title}"): "${signal.snippet}"`,
25607
+ artifactId: linkedQuestion.id,
25608
+ jiraKey: issue2.key
25609
+ });
25610
+ } else {
25611
+ actions.push({
25612
+ type: "question-candidate",
25613
+ description: `Question in ${issue2.key} comment by ${comment.author}: "${signal.snippet}" \u2014 consider creating a question artifact`,
25614
+ jiraKey: issue2.key
25615
+ });
25616
+ }
25617
+ }
25618
+ if (signal.type === "resolution") {
25619
+ const linkedQuestion = issue2.marvinArtifacts.find(
25620
+ (a) => a.type === "question" && a.currentStatus !== "answered"
25621
+ );
25622
+ if (linkedQuestion) {
25623
+ actions.push({
25624
+ type: "resolution-detected",
25625
+ description: `Resolution in ${issue2.key} by ${comment.author} may answer ${linkedQuestion.id} ("${linkedQuestion.title}"): "${signal.snippet}"`,
25626
+ artifactId: linkedQuestion.id,
25627
+ jiraKey: issue2.key
25628
+ });
25629
+ }
25630
+ }
25631
+ }
25632
+ }
25633
+ for (const cl of issue2.confluenceLinks) {
25634
+ actions.push({
25635
+ type: "confluence-review",
25636
+ description: `Confluence page "${cl.title}" linked from ${issue2.key} \u2014 review for relevant updates`,
25637
+ jiraKey: issue2.key
25638
+ });
25639
+ }
25640
+ }
25641
+ return actions;
24958
25642
  }
24959
25643
 
24960
25644
  // src/skills/builtin/jira/tools.ts
@@ -24998,6 +25682,7 @@ function findByJiraKey(store, jiraKey) {
24998
25682
  function createJiraTools(store, projectConfig) {
24999
25683
  const jiraUserConfig = loadUserConfig().jira;
25000
25684
  const defaultProjectKey = projectConfig?.jira?.projectKey;
25685
+ const statusMap = projectConfig?.jira?.statusMap;
25001
25686
  return [
25002
25687
  // --- Local read tools ---
25003
25688
  tool20(
@@ -25158,9 +25843,9 @@ function createJiraTools(store, projectConfig) {
25158
25843
  // --- Local → Jira tools ---
25159
25844
  tool20(
25160
25845
  "push_artifact_to_jira",
25161
- "Create a Jira issue from any Marvin artifact (D/A/Q/F/E) and create a tracking JI-xxx document",
25846
+ "Create a Jira issue from a Marvin artifact. For actions/tasks, links directly via jiraKey on the artifact. For other types, creates a JI-xxx tracking document.",
25162
25847
  {
25163
- artifactId: external_exports.string().describe("Marvin artifact ID (e.g. 'D-001', 'F-003', 'E-002')"),
25848
+ artifactId: external_exports.string().describe("Marvin artifact ID (e.g. 'D-001', 'A-003', 'T-002')"),
25164
25849
  projectKey: external_exports.string().optional().describe("Jira project key (e.g. 'PROJ'). Falls back to jira.projectKey from .marvin/config.yaml if not provided."),
25165
25850
  issueType: external_exports.enum(["Story", "Task", "Bug", "Epic"]).optional().describe("Jira issue type (default: 'Task')")
25166
25851
  },
@@ -25201,6 +25886,24 @@ function createJiraTools(store, projectConfig) {
25201
25886
  description,
25202
25887
  issuetype: { name: args.issueType ?? "Task" }
25203
25888
  });
25889
+ const isDirectLink = artifact.frontmatter.type === "action" || artifact.frontmatter.type === "task";
25890
+ if (isDirectLink) {
25891
+ const existingTags = artifact.frontmatter.tags ?? [];
25892
+ store.update(args.artifactId, {
25893
+ jiraKey: jiraResult.key,
25894
+ jiraUrl: `https://${jira.host}/browse/${jiraResult.key}`,
25895
+ lastJiraSyncAt: (/* @__PURE__ */ new Date()).toISOString(),
25896
+ tags: [...existingTags.filter((t) => !t.startsWith("jira:")), `jira:${jiraResult.key}`]
25897
+ });
25898
+ return {
25899
+ content: [
25900
+ {
25901
+ type: "text",
25902
+ text: `Created Jira ${jiraResult.key} from ${args.artifactId}. Linked directly on the artifact.`
25903
+ }
25904
+ ]
25905
+ };
25906
+ }
25204
25907
  const jiDoc = store.create(
25205
25908
  JIRA_TYPE,
25206
25909
  {
@@ -25322,65 +26025,430 @@ function createJiraTools(store, projectConfig) {
25322
26025
  ]
25323
26026
  };
25324
26027
  }
26028
+ ),
26029
+ // --- Direct Jira linking for actions/tasks ---
26030
+ tool20(
26031
+ "link_to_jira",
26032
+ "Link an existing Jira issue to a Marvin action or task (sets jiraKey directly on the artifact)",
26033
+ {
26034
+ artifactId: external_exports.string().describe("Marvin artifact ID (e.g. 'A-001', 'T-003')"),
26035
+ jiraKey: external_exports.string().describe("Jira issue key (e.g. 'PROJ-123')")
26036
+ },
26037
+ async (args) => {
26038
+ const jira = createJiraClient(jiraUserConfig);
26039
+ if (!jira) return jiraNotConfiguredError();
26040
+ const artifact = store.get(args.artifactId);
26041
+ if (!artifact) {
26042
+ return {
26043
+ content: [
26044
+ { type: "text", text: `Artifact ${args.artifactId} not found` }
26045
+ ],
26046
+ isError: true
26047
+ };
26048
+ }
26049
+ if (artifact.frontmatter.type !== "action" && artifact.frontmatter.type !== "task") {
26050
+ return {
26051
+ content: [
26052
+ {
26053
+ type: "text",
26054
+ text: `link_to_jira only supports action and task artifacts. ${args.artifactId} is type "${artifact.frontmatter.type}". Use link_artifact_to_jira for JI-xxx documents instead.`
26055
+ }
26056
+ ],
26057
+ isError: true
26058
+ };
26059
+ }
26060
+ const issue2 = await jira.client.getIssue(args.jiraKey);
26061
+ const existingTags = artifact.frontmatter.tags ?? [];
26062
+ store.update(args.artifactId, {
26063
+ jiraKey: args.jiraKey,
26064
+ jiraUrl: `https://${jira.host}/browse/${args.jiraKey}`,
26065
+ lastJiraSyncAt: (/* @__PURE__ */ new Date()).toISOString(),
26066
+ tags: [...existingTags.filter((t) => !t.startsWith("jira:")), `jira:${args.jiraKey}`]
26067
+ });
26068
+ return {
26069
+ content: [
26070
+ {
26071
+ type: "text",
26072
+ text: `Linked ${args.artifactId} to Jira ${args.jiraKey} ("${issue2.fields.summary}").`
26073
+ }
26074
+ ]
26075
+ };
26076
+ }
26077
+ ),
26078
+ // --- Jira status fetch (read-only) ---
26079
+ tool20(
26080
+ "fetch_jira_status",
26081
+ "Fetch current Jira status for actions/tasks with jiraKey. Read-only \u2014 returns proposed changes for review. Use update_action/update_task to apply changes.",
26082
+ {
26083
+ artifactId: external_exports.string().optional().describe("Specific artifact ID to check, or omit to check all Jira-linked actions/tasks")
26084
+ },
26085
+ async (args) => {
26086
+ const jira = createJiraClient(jiraUserConfig);
26087
+ if (!jira) return jiraNotConfiguredError();
26088
+ const fetchResult = await fetchJiraStatus(
26089
+ store,
26090
+ jira.client,
26091
+ jira.host,
26092
+ args.artifactId,
26093
+ statusMap
26094
+ );
26095
+ const parts = [];
26096
+ if (fetchResult.artifacts.length > 0) {
26097
+ for (const a of fetchResult.artifacts) {
26098
+ const changes = [];
26099
+ if (a.statusChanged) {
26100
+ changes.push(`status: ${a.currentMarvinStatus} \u2192 ${a.proposedMarvinStatus}`);
26101
+ }
26102
+ if (a.progressChanged) {
26103
+ changes.push(`progress: ${a.currentProgress ?? 0}% \u2192 ${a.proposedProgress}%`);
26104
+ }
26105
+ const header = `${a.id} (${a.jiraKey}) \u2014 Jira: "${a.jiraSummary}" [${a.jiraStatus}]`;
26106
+ if (changes.length > 0) {
26107
+ parts.push(`${header}
26108
+ Proposed changes: ${changes.join(", ")}`);
26109
+ } else {
26110
+ parts.push(`${header}
26111
+ No status/progress changes.`);
26112
+ }
26113
+ if (a.linkedIssues.length > 0) {
26114
+ const done = a.linkedIssues.filter((l) => l.isDone).length;
26115
+ parts.push(` Linked issues (${done}/${a.linkedIssues.length} done):`);
26116
+ for (const li of a.linkedIssues) {
26117
+ const icon = li.isDone ? "\u2713" : "\u25CB";
26118
+ parts.push(` ${icon} ${li.key} ${li.summary} [${li.relationship}] \u2014 ${li.status}`);
26119
+ }
26120
+ }
26121
+ }
26122
+ parts.push("");
26123
+ parts.push("This is a read-only preview. Use update_action or update_task to apply the proposed status/progress changes.");
26124
+ }
26125
+ if (fetchResult.errors.length > 0) {
26126
+ parts.push("Errors:");
26127
+ for (const err of fetchResult.errors) {
26128
+ parts.push(` ${err}`);
26129
+ }
26130
+ }
26131
+ if (fetchResult.artifacts.length === 0 && fetchResult.errors.length === 0) {
26132
+ parts.push("No Jira-linked actions/tasks found.");
26133
+ }
26134
+ return {
26135
+ content: [{ type: "text", text: parts.join("\n") }],
26136
+ isError: fetchResult.errors.length > 0 && fetchResult.artifacts.length === 0
26137
+ };
26138
+ },
26139
+ { annotations: { readOnlyHint: true } }
26140
+ ),
26141
+ // --- Jira status discovery ---
26142
+ tool20(
26143
+ "fetch_jira_statuses",
26144
+ "Fetch all distinct issue statuses from a Jira project and show which are mapped vs unmapped to Marvin statuses. Helps configure jira.statusMap in .marvin/config.yaml.",
26145
+ {
26146
+ projectKey: external_exports.string().optional().describe("Jira project key (e.g. 'MCB1'). Falls back to jira.projectKey from config."),
26147
+ maxResults: external_exports.number().optional().describe("Max issues to scan (default 100)")
26148
+ },
26149
+ async (args) => {
26150
+ const resolvedProjectKey = args.projectKey ?? defaultProjectKey;
26151
+ if (!resolvedProjectKey) {
26152
+ return {
26153
+ content: [
26154
+ {
26155
+ type: "text",
26156
+ text: "No projectKey provided and no default configured."
26157
+ }
26158
+ ],
26159
+ isError: true
26160
+ };
26161
+ }
26162
+ const jira = createJiraClient(jiraUserConfig);
26163
+ if (!jira) return jiraNotConfiguredError();
26164
+ const host = jira.host;
26165
+ const auth = "Basic " + Buffer.from(
26166
+ `${jiraUserConfig?.email ?? process.env.JIRA_EMAIL}:${jiraUserConfig?.apiToken ?? process.env.JIRA_API_TOKEN}`
26167
+ ).toString("base64");
26168
+ const params = new URLSearchParams({
26169
+ jql: `project = ${resolvedProjectKey}`,
26170
+ maxResults: String(args.maxResults ?? 100),
26171
+ fields: "status"
26172
+ });
26173
+ const resp = await fetch(`https://${host}/rest/api/3/search/jql?${params}`, {
26174
+ headers: { Authorization: auth, Accept: "application/json" }
26175
+ });
26176
+ if (!resp.ok) {
26177
+ const text = await resp.text().catch(() => "");
26178
+ return {
26179
+ content: [
26180
+ {
26181
+ type: "text",
26182
+ text: `Jira API error ${resp.status}: ${text}`
26183
+ }
26184
+ ],
26185
+ isError: true
26186
+ };
26187
+ }
26188
+ const data = await resp.json();
26189
+ const statusCounts = /* @__PURE__ */ new Map();
26190
+ for (const issue2 of data.issues) {
26191
+ const s = issue2.fields.status.name;
26192
+ statusCounts.set(s, (statusCounts.get(s) ?? 0) + 1);
26193
+ }
26194
+ const actionMap = statusMap?.action ?? DEFAULT_ACTION_STATUS_MAP;
26195
+ const taskMap = statusMap?.task ?? DEFAULT_TASK_STATUS_MAP;
26196
+ const actionLookup = /* @__PURE__ */ new Map();
26197
+ for (const [marvin, jiraStatuses] of Object.entries(actionMap)) {
26198
+ for (const js of jiraStatuses) actionLookup.set(js.toLowerCase(), marvin);
26199
+ }
26200
+ const taskLookup = /* @__PURE__ */ new Map();
26201
+ for (const [marvin, jiraStatuses] of Object.entries(taskMap)) {
26202
+ for (const js of jiraStatuses) taskLookup.set(js.toLowerCase(), marvin);
26203
+ }
26204
+ const parts = [
26205
+ `Found ${statusCounts.size} distinct statuses in ${resolvedProjectKey} (scanned ${data.issues.length} of ${data.total} issues):`,
26206
+ ""
26207
+ ];
26208
+ const sorted = [...statusCounts.entries()].sort((a, b) => b[1] - a[1]);
26209
+ const unmappedAction = [];
26210
+ const unmappedTask = [];
26211
+ for (const [status, count] of sorted) {
26212
+ const actionTarget = actionLookup.get(status.toLowerCase());
26213
+ const taskTarget = taskLookup.get(status.toLowerCase());
26214
+ const actionLabel = actionTarget ? `\u2192 ${actionTarget}` : "UNMAPPED (\u2192 open)";
26215
+ const taskLabel = taskTarget ? `\u2192 ${taskTarget}` : "UNMAPPED (\u2192 backlog)";
26216
+ parts.push(` ${status} (${count} issues)`);
26217
+ parts.push(` action: ${actionLabel}`);
26218
+ parts.push(` task: ${taskLabel}`);
26219
+ if (!actionTarget) unmappedAction.push(status);
26220
+ if (!taskTarget) unmappedTask.push(status);
26221
+ }
26222
+ if (unmappedAction.length > 0 || unmappedTask.length > 0) {
26223
+ parts.push("");
26224
+ parts.push("To fix unmapped statuses, add jira.statusMap to .marvin/config.yaml:");
26225
+ parts.push(" jira:");
26226
+ parts.push(" statusMap:");
26227
+ if (unmappedAction.length > 0) {
26228
+ parts.push(" action:");
26229
+ parts.push(` # Map these: ${unmappedAction.join(", ")}`);
26230
+ parts.push(" # <marvin-status>: [<jira-status>, ...]");
26231
+ }
26232
+ if (unmappedTask.length > 0) {
26233
+ parts.push(" task:");
26234
+ parts.push(` # Map these: ${unmappedTask.join(", ")}`);
26235
+ parts.push(" # <marvin-status>: [<jira-status>, ...]");
26236
+ }
26237
+ } else {
26238
+ parts.push("");
26239
+ parts.push("All statuses are mapped.");
26240
+ }
26241
+ const usingConfig = statusMap?.action || statusMap?.task;
26242
+ parts.push("");
26243
+ parts.push(usingConfig ? "Using status maps from .marvin/config.yaml." : "Using built-in default status maps (no jira.statusMap in config).");
26244
+ return {
26245
+ content: [{ type: "text", text: parts.join("\n") }]
26246
+ };
26247
+ },
26248
+ { annotations: { readOnlyHint: true } }
26249
+ ),
26250
+ // --- Jira daily summary ---
26251
+ tool20(
26252
+ "fetch_jira_daily",
26253
+ "Fetch a daily summary of Jira changes: status transitions, comments, linked Confluence pages, and cross-referenced Marvin artifacts. Read-only \u2014 returns proposed actions for review.",
26254
+ {
26255
+ from: external_exports.string().optional().describe("Start date (YYYY-MM-DD). Defaults to today."),
26256
+ to: external_exports.string().optional().describe("End date (YYYY-MM-DD). Defaults to same as 'from'."),
26257
+ projectKey: external_exports.string().optional().describe("Jira project key. Falls back to jira.projectKey from config.")
26258
+ },
26259
+ async (args) => {
26260
+ const resolvedProjectKey = args.projectKey ?? defaultProjectKey;
26261
+ if (!resolvedProjectKey) {
26262
+ return {
26263
+ content: [
26264
+ {
26265
+ type: "text",
26266
+ text: "No projectKey provided and no default configured."
26267
+ }
26268
+ ],
26269
+ isError: true
26270
+ };
26271
+ }
26272
+ const jira = createJiraClient(jiraUserConfig);
26273
+ if (!jira) return jiraNotConfiguredError();
26274
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
26275
+ const fromDate = args.from ?? today;
26276
+ const toDate = args.to ?? fromDate;
26277
+ const daily = await fetchJiraDaily(
26278
+ store,
26279
+ jira.client,
26280
+ jira.host,
26281
+ resolvedProjectKey,
26282
+ { from: fromDate, to: toDate },
26283
+ statusMap
26284
+ );
26285
+ return {
26286
+ content: [{ type: "text", text: formatDailySummary(daily) }],
26287
+ isError: daily.errors.length > 0 && daily.issues.length === 0
26288
+ };
26289
+ },
26290
+ { annotations: { readOnlyHint: true } }
25325
26291
  )
25326
26292
  ];
25327
26293
  }
26294
+ function formatDailySummary(daily) {
26295
+ const parts = [];
26296
+ const rangeLabel = daily.dateRange.from === daily.dateRange.to ? daily.dateRange.from : `${daily.dateRange.from} to ${daily.dateRange.to}`;
26297
+ parts.push(`Jira Daily Summary \u2014 ${daily.projectKey} \u2014 ${rangeLabel}`);
26298
+ parts.push(`${daily.issues.length} issue(s) updated.
26299
+ `);
26300
+ const linked = daily.issues.filter((i) => i.marvinArtifacts.length > 0);
26301
+ const unlinked = daily.issues.filter((i) => i.marvinArtifacts.length === 0);
26302
+ if (linked.length > 0) {
26303
+ parts.push("## Linked Issues (with Marvin artifacts)\n");
26304
+ for (const issue2 of linked) {
26305
+ parts.push(formatIssueEntry(issue2));
26306
+ }
26307
+ }
26308
+ if (unlinked.length > 0) {
26309
+ parts.push("## Unlinked Issues (no Marvin artifact)\n");
26310
+ for (const issue2 of unlinked) {
26311
+ parts.push(formatIssueEntry(issue2));
26312
+ }
26313
+ }
26314
+ if (daily.proposedActions.length > 0) {
26315
+ parts.push("## Proposed Actions\n");
26316
+ for (const action of daily.proposedActions) {
26317
+ const icon = action.type === "status-update" ? "\u21BB" : action.type === "unlinked-issue" ? "+" : action.type === "link-suggestion" ? "\u{1F517}" : action.type === "question-candidate" ? "?" : action.type === "decision-candidate" ? "\u2696" : action.type === "blocker-detected" ? "\u{1F6AB}" : action.type === "resolution-detected" ? "\u2713" : "\u{1F4C4}";
26318
+ parts.push(` ${icon} ${action.description}`);
26319
+ }
26320
+ parts.push("");
26321
+ parts.push("These are suggestions. Use update_action, update_task, or other tools to apply changes.");
26322
+ }
26323
+ if (daily.errors.length > 0) {
26324
+ parts.push("\n## Errors\n");
26325
+ for (const err of daily.errors) {
26326
+ parts.push(` ${err}`);
26327
+ }
26328
+ }
26329
+ return parts.join("\n");
26330
+ }
26331
+ function formatIssueEntry(issue2) {
26332
+ const lines = [];
26333
+ const artifacts = issue2.marvinArtifacts.map((a) => a.id).join(", ");
26334
+ const artifactLabel = artifacts ? ` \u2192 ${artifacts}` : "";
26335
+ lines.push(`### ${issue2.key} \u2014 ${issue2.summary} [${issue2.currentStatus}]${artifactLabel}`);
26336
+ lines.push(` Type: ${issue2.issueType} | Assignee: ${issue2.assignee ?? "unassigned"}`);
26337
+ for (const a of issue2.marvinArtifacts) {
26338
+ if (a.statusDrift) {
26339
+ lines.push(` \u26A0 ${a.id} status drift: Marvin="${a.currentStatus}" vs proposed="${a.proposedStatus}"`);
26340
+ }
26341
+ }
26342
+ if (issue2.changes.length > 0) {
26343
+ lines.push(" Changes:");
26344
+ for (const c of issue2.changes) {
26345
+ lines.push(` ${c.field}: ${c.from ?? "\u2205"} \u2192 ${c.to ?? "\u2205"} (${c.author}, ${c.timestamp.slice(0, 16)})`);
26346
+ }
26347
+ }
26348
+ if (issue2.comments.length > 0) {
26349
+ lines.push(` Comments (${issue2.comments.length}):`);
26350
+ for (const c of issue2.comments) {
26351
+ let signalIcons = "";
26352
+ if (c.signals.length > 0) {
26353
+ const icons = c.signals.map(
26354
+ (s) => s.type === "blocker" ? "\u{1F6AB}" : s.type === "decision" ? "\u2696" : s.type === "question" ? "?" : "\u2713"
26355
+ );
26356
+ signalIcons = ` [${icons.join("")}]`;
26357
+ }
26358
+ lines.push(` ${c.author} (${c.created.slice(0, 16)})${signalIcons}: ${c.bodyPreview}`);
26359
+ }
26360
+ }
26361
+ if (issue2.linkSuggestions.length > 0) {
26362
+ lines.push(" Possible Marvin matches:");
26363
+ for (const s of issue2.linkSuggestions) {
26364
+ lines.push(` \u{1F517} ${s.artifactId} ("${s.artifactTitle}") \u2014 ${Math.round(s.score * 100)}% match [${s.sharedTerms.join(", ")}]`);
26365
+ }
26366
+ }
26367
+ if (issue2.linkedIssues.length > 0) {
26368
+ lines.push(" Linked issues:");
26369
+ for (const li of issue2.linkedIssues) {
26370
+ const icon = li.isDone ? "\u2713" : "\u25CB";
26371
+ lines.push(` ${icon} ${li.key} ${li.summary} [${li.relationship}] \u2014 ${li.status}`);
26372
+ }
26373
+ }
26374
+ if (issue2.confluenceLinks.length > 0) {
26375
+ lines.push(" Confluence pages:");
26376
+ for (const cl of issue2.confluenceLinks) {
26377
+ lines.push(` \u{1F4C4} ${cl.title}: ${cl.url}`);
26378
+ }
26379
+ }
26380
+ lines.push("");
26381
+ return lines.join("\n");
26382
+ }
25328
26383
 
25329
26384
  // src/skills/builtin/jira/index.ts
26385
+ var COMMON_TOOLS = `**Available tools:**
26386
+ - \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues (JI-xxx documents)
26387
+ - \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import issues from Jira by key or JQL query
26388
+ - \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact. For **actions and tasks**, links directly via \`jiraKey\` on the artifact (no JI-xxx intermediary). For other types, creates a JI-xxx tracking document.
26389
+ - \`link_to_jira\` \u2014 link an existing Jira issue to a Marvin action or task (sets \`jiraKey\` directly on the artifact)
26390
+ - \`fetch_jira_status\` \u2014 **read-only**: fetch current Jira status, subtask progress, and linked issues for Jira-linked actions/tasks. Returns proposed changes without applying them.
26391
+ - \`fetch_jira_daily\` \u2014 **read-only**: fetch a daily/range summary of all Jira changes \u2014 status transitions, comments, linked Confluence pages, and cross-references with Marvin artifacts. Returns proposed actions (status updates, unlinked issues, question candidates, Confluence pages to review).
26392
+ - \`fetch_jira_statuses\` \u2014 **read-only**: discover all Jira statuses in a project and show their Marvin mappings (mapped vs unmapped).
26393
+ - \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
26394
+ - \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx`;
26395
+ var COMMON_WORKFLOW = `**Jira sync workflow:**
26396
+ 1. Call \`fetch_jira_status\` to see what Jira reports for linked artifacts
26397
+ 2. Analyze the proposed changes (status transitions, subtask progress, blockers from linked issues)
26398
+ 3. Use \`update_action\` / \`update_task\` to apply the changes you agree with
26399
+
26400
+ **Daily review workflow:**
26401
+ 1. Call \`fetch_jira_daily\` (optionally with \`from\`/\`to\` date range) to get a summary of all Jira activity
26402
+ 2. Review the proposed actions: status updates, unlinked issues to track, questions that may be answered, Confluence pages to review
26403
+ 3. Use existing tools to apply changes, create new artifacts, or link untracked issues`;
25330
26404
  var jiraSkill = {
25331
26405
  id: "jira",
25332
26406
  name: "Jira Integration",
25333
26407
  description: "Bidirectional sync between Marvin artifacts and Jira issues",
25334
26408
  version: "1.0.0",
25335
26409
  format: "builtin-ts",
25336
- // No default persona affinity — opt-in via config.yaml skills section
25337
26410
  documentTypeRegistrations: [
25338
26411
  { type: "jira-issue", dirName: "jira-issues", idPrefix: "JI" }
25339
26412
  ],
25340
26413
  tools: (store, projectConfig) => createJiraTools(store, projectConfig),
25341
26414
  promptFragments: {
25342
- "product-owner": `You have the **Jira Integration** skill. You can pull issues from Jira and push Marvin artifacts to Jira.
26415
+ "product-owner": `You have the **Jira Integration** skill.
25343
26416
 
25344
- **Available tools:**
25345
- - \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues
25346
- - \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import issues from Jira by key or JQL query
25347
- - \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, feature, etc.). The \`projectKey\` parameter is optional when a default is configured in \`.marvin/config.yaml\` under \`jira.projectKey\`.
25348
- - \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
25349
- - \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx
26417
+ ${COMMON_TOOLS}
26418
+
26419
+ ${COMMON_WORKFLOW}
25350
26420
 
25351
26421
  **As Product Owner, use Jira integration to:**
26422
+ - Use \`fetch_jira_daily\` for daily standups \u2014 review what changed, identify status drift, spot untracked work
25352
26423
  - Pull stakeholder-reported issues for triage and prioritization
25353
26424
  - Push approved features as Stories for development tracking
25354
26425
  - Link decisions to Jira issues for audit trail and traceability
25355
- - Use JQL queries to review backlog status (e.g. \`project = PROJ AND status = "To Do"\`)`,
25356
- "tech-lead": `You have the **Jira Integration** skill. You can pull issues from Jira and push Marvin artifacts to Jira.
26426
+ - Use \`fetch_jira_statuses\` when setting up a new project to configure status mappings`,
26427
+ "tech-lead": `You have the **Jira Integration** skill.
25357
26428
 
25358
- **Available tools:**
25359
- - \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues
25360
- - \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import issues from Jira by key or JQL query
25361
- - \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, action, epic, task, etc.). The \`projectKey\` parameter is optional when a default is configured in \`.marvin/config.yaml\` under \`jira.projectKey\`.
25362
- - \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
25363
- - \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx
26429
+ ${COMMON_TOOLS}
26430
+
26431
+ ${COMMON_WORKFLOW}
25364
26432
 
25365
26433
  **As Tech Lead, use Jira integration to:**
26434
+ - Use \`fetch_jira_daily\` to review technical progress \u2014 status transitions, new comments, Confluence design docs
25366
26435
  - Pull technical issues and bugs for sprint planning and estimation
25367
26436
  - Push epics, tasks, and technical decisions to Jira for cross-team visibility
25368
- - Bidirectional sync to keep local governance and Jira in alignment
25369
- - Use JQL queries to track technical debt (e.g. \`labels = "tech-debt" AND status != "Done"\`)`,
25370
- "delivery-manager": `You have the **Jira Integration** skill. You can pull issues from Jira and push Marvin artifacts to Jira.
26437
+ - Use \`link_to_jira\` to connect Marvin tasks to existing Jira tickets
26438
+ - Use \`fetch_jira_statuses\` to verify status mappings match the team's Jira workflow`,
26439
+ "delivery-manager": `You have the **Jira Integration** skill.
25371
26440
 
25372
- **Available tools:**
25373
- - \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues
25374
- - \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import issues from Jira by key or JQL query
25375
- - \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, action, etc.). The \`projectKey\` parameter is optional when a default is configured in \`.marvin/config.yaml\` under \`jira.projectKey\`.
25376
- - \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
25377
- - \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx
26441
+ ${COMMON_TOOLS}
26442
+
26443
+ ${COMMON_WORKFLOW}
26444
+ This is a third path for progress tracking alongside Contributions and Meetings.
25378
26445
 
25379
26446
  **As Delivery Manager, use Jira integration to:**
26447
+ - Use \`fetch_jira_daily\` for daily progress reports \u2014 track what moved, identify blockers, spot untracked work
25380
26448
  - Pull sprint issues for tracking progress and blockers
25381
- - Push actions, decisions, and tasks to Jira for stakeholder visibility
25382
- - Use JQL queries for reporting (e.g. \`sprint in openSprints() AND assignee = currentUser()\`)
25383
- - Sync status between Marvin governance items and Jira issues`
26449
+ - Push actions and tasks to Jira for stakeholder visibility
26450
+ - Use \`fetch_jira_daily\` with a date range for sprint retrospectives (e.g. \`from: "2026-03-10", to: "2026-03-21"\`)
26451
+ - Use \`fetch_jira_statuses\` to ensure Jira workflow statuses are properly mapped`
25384
26452
  }
25385
26453
  };
25386
26454
 
@@ -30498,12 +31566,355 @@ Run "marvin doctor --fix" to auto-repair fixable issues.`));
30498
31566
  console.log();
30499
31567
  }
30500
31568
 
31569
+ // src/cli/commands/jira.ts
31570
+ import chalk20 from "chalk";
31571
+ async function jiraSyncCommand(artifactId, options = {}) {
31572
+ const project = loadProject();
31573
+ const plugin = resolvePlugin(project.config.methodology);
31574
+ const registrations = plugin?.documentTypeRegistrations ?? [];
31575
+ const jiReg = { type: "jira-issue", dirName: "jira-issues", idPrefix: "JI" };
31576
+ const store = new DocumentStore(project.marvinDir, [...registrations, jiReg]);
31577
+ const jiraUserConfig = loadUserConfig().jira;
31578
+ const jira = createJiraClient(jiraUserConfig);
31579
+ if (!jira) {
31580
+ console.log(
31581
+ chalk20.red(
31582
+ 'Jira is not configured. Run "marvin config jira" or set JIRA_HOST, JIRA_EMAIL, and JIRA_API_TOKEN environment variables.'
31583
+ )
31584
+ );
31585
+ return;
31586
+ }
31587
+ const statusMap = project.config.jira?.statusMap;
31588
+ const label = artifactId ? `Checking ${artifactId} against Jira...` : "Checking all Jira-linked actions/tasks...";
31589
+ console.log(chalk20.dim(label));
31590
+ if (options.dryRun) {
31591
+ const fetchResult = await fetchJiraStatus(
31592
+ store,
31593
+ jira.client,
31594
+ jira.host,
31595
+ artifactId,
31596
+ statusMap
31597
+ );
31598
+ const withChanges = fetchResult.artifacts.filter(
31599
+ (a) => a.statusChanged || a.progressChanged
31600
+ );
31601
+ const noChanges = fetchResult.artifacts.filter(
31602
+ (a) => !a.statusChanged && !a.progressChanged
31603
+ );
31604
+ if (withChanges.length > 0) {
31605
+ console.log(chalk20.yellow(`
31606
+ Proposed changes for ${withChanges.length} artifact(s):`));
31607
+ for (const a of withChanges) {
31608
+ console.log(` ${chalk20.bold(a.id)} (${a.jiraKey}) \u2014 Jira: "${a.jiraSummary}"`);
31609
+ if (a.statusChanged) {
31610
+ console.log(
31611
+ ` status: ${chalk20.yellow(a.currentMarvinStatus)} \u2192 ${chalk20.green(a.proposedMarvinStatus)}`
31612
+ );
31613
+ }
31614
+ if (a.progressChanged) {
31615
+ console.log(
31616
+ ` progress: ${chalk20.yellow(String(a.currentProgress ?? 0) + "%")} \u2192 ${chalk20.green(String(a.proposedProgress) + "%")}`
31617
+ );
31618
+ }
31619
+ if (a.linkedIssues.length > 0) {
31620
+ const done = a.linkedIssues.filter((l) => l.isDone).length;
31621
+ console.log(chalk20.dim(` ${done}/${a.linkedIssues.length} linked issues done`));
31622
+ }
31623
+ }
31624
+ console.log(chalk20.dim("\nRun without --dry-run to apply these changes."));
31625
+ }
31626
+ if (noChanges.length > 0) {
31627
+ console.log(chalk20.dim(`
31628
+ ${noChanges.length} artifact(s) already in sync.`));
31629
+ }
31630
+ if (fetchResult.errors.length > 0) {
31631
+ console.log(chalk20.red("\nErrors:"));
31632
+ for (const err of fetchResult.errors) {
31633
+ console.log(chalk20.red(` ${err}`));
31634
+ }
31635
+ }
31636
+ if (fetchResult.artifacts.length === 0 && fetchResult.errors.length === 0) {
31637
+ console.log(chalk20.dim("\nNo Jira-linked actions/tasks found to check."));
31638
+ }
31639
+ return;
31640
+ }
31641
+ const result = await syncJiraProgress(
31642
+ store,
31643
+ jira.client,
31644
+ jira.host,
31645
+ artifactId,
31646
+ statusMap
31647
+ );
31648
+ if (result.updated.length > 0) {
31649
+ console.log(chalk20.green(`
31650
+ Updated ${result.updated.length} artifact(s):`));
31651
+ for (const entry of result.updated) {
31652
+ const statusChange = entry.oldStatus !== entry.newStatus ? `${chalk20.yellow(entry.oldStatus)} \u2192 ${chalk20.green(entry.newStatus)}` : chalk20.dim(entry.newStatus);
31653
+ console.log(` ${chalk20.bold(entry.id)} (${entry.jiraKey}): ${statusChange}`);
31654
+ if (entry.linkedIssues.length > 0) {
31655
+ const done = entry.linkedIssues.filter((l) => l.isDone).length;
31656
+ console.log(
31657
+ chalk20.dim(` ${done}/${entry.linkedIssues.length} linked issues done`)
31658
+ );
31659
+ for (const li of entry.linkedIssues) {
31660
+ const icon = li.isDone ? chalk20.green("\u2713") : chalk20.dim("\u25CB");
31661
+ console.log(
31662
+ chalk20.dim(` ${icon} ${li.key} ${li.summary} [${li.relationship}]`)
31663
+ );
31664
+ }
31665
+ }
31666
+ }
31667
+ }
31668
+ if (result.unchanged > 0) {
31669
+ console.log(chalk20.dim(`
31670
+ ${result.unchanged} artifact(s) unchanged.`));
31671
+ }
31672
+ if (result.errors.length > 0) {
31673
+ console.log(chalk20.red("\nErrors:"));
31674
+ for (const err of result.errors) {
31675
+ console.log(chalk20.red(` ${err}`));
31676
+ }
31677
+ }
31678
+ if (result.updated.length === 0 && result.unchanged === 0 && result.errors.length === 0) {
31679
+ console.log(chalk20.dim("\nNo Jira-linked actions/tasks found to sync."));
31680
+ }
31681
+ }
31682
+ async function jiraStatusesCommand(projectKey) {
31683
+ const project = loadProject();
31684
+ const jiraUserConfig = loadUserConfig().jira;
31685
+ const jira = createJiraClient(jiraUserConfig);
31686
+ if (!jira) {
31687
+ console.log(
31688
+ chalk20.red(
31689
+ 'Jira is not configured. Run "marvin config jira" or set JIRA_HOST, JIRA_EMAIL, and JIRA_API_TOKEN environment variables.'
31690
+ )
31691
+ );
31692
+ return;
31693
+ }
31694
+ const resolvedProjectKey = projectKey ?? project.config.jira?.projectKey;
31695
+ if (!resolvedProjectKey) {
31696
+ console.log(
31697
+ chalk20.red(
31698
+ "No project key provided. Pass it as an argument or set jira.projectKey in .marvin/config.yaml."
31699
+ )
31700
+ );
31701
+ return;
31702
+ }
31703
+ console.log(chalk20.dim(`Fetching statuses from Jira project ${resolvedProjectKey}...`));
31704
+ const statusMap = project.config.jira?.statusMap;
31705
+ const actionMap = statusMap?.action ?? DEFAULT_ACTION_STATUS_MAP;
31706
+ const taskMap = statusMap?.task ?? DEFAULT_TASK_STATUS_MAP;
31707
+ const email3 = jiraUserConfig?.email ?? process.env.JIRA_EMAIL;
31708
+ const apiToken = jiraUserConfig?.apiToken ?? process.env.JIRA_API_TOKEN;
31709
+ const auth = "Basic " + Buffer.from(`${email3}:${apiToken}`).toString("base64");
31710
+ const params = new URLSearchParams({
31711
+ jql: `project = ${resolvedProjectKey}`,
31712
+ maxResults: "100",
31713
+ fields: "status"
31714
+ });
31715
+ const resp = await fetch(`https://${jira.host}/rest/api/3/search/jql?${params}`, {
31716
+ headers: { Authorization: auth, Accept: "application/json" }
31717
+ });
31718
+ if (!resp.ok) {
31719
+ const text = await resp.text().catch(() => "");
31720
+ console.log(chalk20.red(`Jira API error ${resp.status}: ${text}`));
31721
+ return;
31722
+ }
31723
+ const data = await resp.json();
31724
+ const statusCounts = /* @__PURE__ */ new Map();
31725
+ for (const issue2 of data.issues) {
31726
+ const s = issue2.fields.status.name;
31727
+ statusCounts.set(s, (statusCounts.get(s) ?? 0) + 1);
31728
+ }
31729
+ const actionLookup = /* @__PURE__ */ new Map();
31730
+ for (const [marvin, jiraStatuses] of Object.entries(actionMap)) {
31731
+ for (const js of jiraStatuses) actionLookup.set(js.toLowerCase(), marvin);
31732
+ }
31733
+ const taskLookup = /* @__PURE__ */ new Map();
31734
+ for (const [marvin, jiraStatuses] of Object.entries(taskMap)) {
31735
+ for (const js of jiraStatuses) taskLookup.set(js.toLowerCase(), marvin);
31736
+ }
31737
+ console.log(
31738
+ `
31739
+ Found ${chalk20.bold(String(statusCounts.size))} distinct statuses in ${chalk20.bold(resolvedProjectKey)} (scanned ${data.issues.length} of ${data.total} issues):
31740
+ `
31741
+ );
31742
+ const sorted = [...statusCounts.entries()].sort((a, b) => b[1] - a[1]);
31743
+ let hasUnmapped = false;
31744
+ for (const [status, count] of sorted) {
31745
+ const actionTarget = actionLookup.get(status.toLowerCase());
31746
+ const taskTarget = taskLookup.get(status.toLowerCase());
31747
+ const actionLabel = actionTarget ? chalk20.green(`\u2192 ${actionTarget}`) : chalk20.yellow("UNMAPPED (\u2192 open)");
31748
+ const taskLabel = taskTarget ? chalk20.green(`\u2192 ${taskTarget}`) : chalk20.yellow("UNMAPPED (\u2192 backlog)");
31749
+ if (!actionTarget || !taskTarget) hasUnmapped = true;
31750
+ console.log(` ${chalk20.bold(status)} ${chalk20.dim(`(${count} issues)`)}`);
31751
+ console.log(` action: ${actionLabel}`);
31752
+ console.log(` task: ${taskLabel}`);
31753
+ }
31754
+ if (hasUnmapped) {
31755
+ console.log(chalk20.yellow("\nSome statuses are unmapped. Add jira.statusMap to .marvin/config.yaml:"));
31756
+ console.log(chalk20.dim(" jira:"));
31757
+ console.log(chalk20.dim(" statusMap:"));
31758
+ console.log(chalk20.dim(" action:"));
31759
+ console.log(chalk20.dim(' <marvin-status>: ["<Jira Status>", ...]'));
31760
+ console.log(chalk20.dim(" task:"));
31761
+ console.log(chalk20.dim(' <marvin-status>: ["<Jira Status>", ...]'));
31762
+ } else {
31763
+ console.log(chalk20.green("\nAll statuses are mapped."));
31764
+ }
31765
+ const usingConfig = statusMap?.action || statusMap?.task;
31766
+ console.log(
31767
+ chalk20.dim(
31768
+ usingConfig ? "\nUsing status maps from .marvin/config.yaml." : "\nUsing built-in default status maps (no jira.statusMap in config)."
31769
+ )
31770
+ );
31771
+ }
31772
+ async function jiraDailyCommand(options) {
31773
+ const proj = loadProject();
31774
+ const plugin = resolvePlugin(proj.config.methodology);
31775
+ const registrations = plugin?.documentTypeRegistrations ?? [];
31776
+ const jiReg = { type: "jira-issue", dirName: "jira-issues", idPrefix: "JI" };
31777
+ const store = new DocumentStore(proj.marvinDir, [...registrations, jiReg]);
31778
+ const jiraUserConfig = loadUserConfig().jira;
31779
+ const jira = createJiraClient(jiraUserConfig);
31780
+ if (!jira) {
31781
+ console.log(
31782
+ chalk20.red(
31783
+ 'Jira is not configured. Run "marvin config jira" or set JIRA_HOST, JIRA_EMAIL, and JIRA_API_TOKEN environment variables.'
31784
+ )
31785
+ );
31786
+ return;
31787
+ }
31788
+ const resolvedProjectKey = options.project ?? proj.config.jira?.projectKey;
31789
+ if (!resolvedProjectKey) {
31790
+ console.log(
31791
+ chalk20.red(
31792
+ "No project key provided. Use --project or set jira.projectKey in .marvin/config.yaml."
31793
+ )
31794
+ );
31795
+ return;
31796
+ }
31797
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
31798
+ const fromDate = options.from ?? today;
31799
+ const toDate = options.to ?? fromDate;
31800
+ const statusMap = proj.config.jira?.statusMap;
31801
+ const rangeLabel = fromDate === toDate ? fromDate : `${fromDate} to ${toDate}`;
31802
+ console.log(
31803
+ chalk20.dim(`Fetching Jira daily summary for ${resolvedProjectKey} \u2014 ${rangeLabel}...`)
31804
+ );
31805
+ const daily = await fetchJiraDaily(
31806
+ store,
31807
+ jira.client,
31808
+ jira.host,
31809
+ resolvedProjectKey,
31810
+ { from: fromDate, to: toDate },
31811
+ statusMap
31812
+ );
31813
+ console.log(
31814
+ `
31815
+ ${chalk20.bold(`Jira Daily \u2014 ${resolvedProjectKey} \u2014 ${rangeLabel}`)}`
31816
+ );
31817
+ console.log(`${daily.issues.length} issue(s) updated.
31818
+ `);
31819
+ const linked = daily.issues.filter((i) => i.marvinArtifacts.length > 0);
31820
+ const unlinked = daily.issues.filter((i) => i.marvinArtifacts.length === 0);
31821
+ if (linked.length > 0) {
31822
+ console.log(chalk20.underline("Linked Issues (with Marvin artifacts):\n"));
31823
+ for (const issue2 of linked) {
31824
+ printIssueEntry(issue2);
31825
+ }
31826
+ }
31827
+ if (unlinked.length > 0) {
31828
+ console.log(chalk20.underline("Unlinked Issues (no Marvin artifact):\n"));
31829
+ for (const issue2 of unlinked) {
31830
+ printIssueEntry(issue2);
31831
+ }
31832
+ }
31833
+ if (daily.proposedActions.length > 0) {
31834
+ console.log(chalk20.underline("Proposed Actions:\n"));
31835
+ for (const action of daily.proposedActions) {
31836
+ const icon = action.type === "status-update" ? chalk20.yellow("\u21BB") : action.type === "unlinked-issue" ? chalk20.blue("+") : action.type === "link-suggestion" ? chalk20.cyan("\u{1F517}") : action.type === "question-candidate" ? chalk20.magenta("?") : action.type === "decision-candidate" ? chalk20.yellow("\u2696") : action.type === "blocker-detected" ? chalk20.red("\u{1F6AB}") : action.type === "resolution-detected" ? chalk20.green("\u2713") : chalk20.cyan("\u{1F4C4}");
31837
+ console.log(` ${icon} ${action.description}`);
31838
+ }
31839
+ console.log();
31840
+ }
31841
+ if (daily.errors.length > 0) {
31842
+ console.log(chalk20.red("Errors:"));
31843
+ for (const err of daily.errors) {
31844
+ console.log(chalk20.red(` ${err}`));
31845
+ }
31846
+ }
31847
+ if (daily.issues.length === 0 && daily.errors.length === 0) {
31848
+ console.log(chalk20.dim("No Jira activity found for this period."));
31849
+ }
31850
+ }
31851
+ function printIssueEntry(issue2) {
31852
+ const artifacts = issue2.marvinArtifacts.map((a) => a.id).join(", ");
31853
+ const artifactLabel = artifacts ? chalk20.cyan(` \u2192 ${artifacts}`) : "";
31854
+ console.log(
31855
+ ` ${chalk20.bold(issue2.key)} \u2014 ${issue2.summary} [${chalk20.yellow(issue2.currentStatus)}]${artifactLabel}`
31856
+ );
31857
+ console.log(
31858
+ chalk20.dim(` Type: ${issue2.issueType} | Assignee: ${issue2.assignee ?? "unassigned"}`)
31859
+ );
31860
+ for (const a of issue2.marvinArtifacts) {
31861
+ if (a.statusDrift) {
31862
+ console.log(
31863
+ chalk20.yellow(` \u26A0 ${a.id} status drift: Marvin="${a.currentStatus}" vs proposed="${a.proposedStatus}"`)
31864
+ );
31865
+ }
31866
+ }
31867
+ if (issue2.changes.length > 0) {
31868
+ console.log(chalk20.dim(" Changes:"));
31869
+ for (const c of issue2.changes) {
31870
+ console.log(
31871
+ chalk20.dim(` ${c.field}: ${c.from ?? "\u2205"} \u2192 ${c.to ?? "\u2205"} (${c.author}, ${c.timestamp.slice(0, 16)})`)
31872
+ );
31873
+ }
31874
+ }
31875
+ if (issue2.comments.length > 0) {
31876
+ console.log(chalk20.dim(` Comments (${issue2.comments.length}):`));
31877
+ for (const c of issue2.comments) {
31878
+ let signalLabel = "";
31879
+ if (c.signals.length > 0) {
31880
+ const labels = c.signals.map(
31881
+ (s) => s.type === "blocker" ? chalk20.red("\u{1F6AB}blocker") : s.type === "decision" ? chalk20.yellow("\u2696decision") : s.type === "question" ? chalk20.magenta("?question") : chalk20.green("\u2713resolution")
31882
+ );
31883
+ signalLabel = ` ${labels.join(" ")}`;
31884
+ }
31885
+ console.log(chalk20.dim(` ${c.author} (${c.created.slice(0, 16)})${signalLabel}: ${c.bodyPreview}`));
31886
+ }
31887
+ }
31888
+ if (issue2.linkSuggestions.length > 0) {
31889
+ console.log(chalk20.cyan(" Possible Marvin matches:"));
31890
+ for (const s of issue2.linkSuggestions) {
31891
+ console.log(
31892
+ chalk20.cyan(` \u{1F517} ${s.artifactId} ("${s.artifactTitle}") \u2014 ${Math.round(s.score * 100)}% match [${s.sharedTerms.join(", ")}]`)
31893
+ );
31894
+ }
31895
+ }
31896
+ if (issue2.linkedIssues.length > 0) {
31897
+ console.log(chalk20.dim(" Linked issues:"));
31898
+ for (const li of issue2.linkedIssues) {
31899
+ const icon = li.isDone ? chalk20.green("\u2713") : chalk20.dim("\u25CB");
31900
+ console.log(chalk20.dim(` ${icon} ${li.key} ${li.summary} [${li.relationship}] \u2014 ${li.status}`));
31901
+ }
31902
+ }
31903
+ if (issue2.confluenceLinks.length > 0) {
31904
+ console.log(chalk20.dim(" Confluence pages:"));
31905
+ for (const cl of issue2.confluenceLinks) {
31906
+ console.log(chalk20.dim(` \u{1F4C4} ${cl.title}: ${cl.url}`));
31907
+ }
31908
+ }
31909
+ console.log();
31910
+ }
31911
+
30501
31912
  // src/cli/program.ts
30502
31913
  function createProgram() {
30503
31914
  const program = new Command();
30504
31915
  program.name("marvin").description(
30505
31916
  "AI-powered product development assistant with Product Owner, Delivery Manager, and Technical Lead personas"
30506
- ).version("0.5.7");
31917
+ ).version("0.5.9");
30507
31918
  program.command("init").description("Initialize a new Marvin project in the current directory").action(async () => {
30508
31919
  await initCommand();
30509
31920
  });
@@ -30599,6 +32010,16 @@ function createProgram() {
30599
32010
  generateCmd.command("claude-md").description("Generate .marvin/CLAUDE.md project instruction file").option("--force", "Overwrite existing file without prompting").action(async (options) => {
30600
32011
  await generateClaudeMdCommand(options);
30601
32012
  });
32013
+ const jiraCmd = program.command("jira").description("Jira integration commands");
32014
+ jiraCmd.command("sync [artifactId]").description("Sync Jira-linked actions/tasks with their Jira issues").option("--dry-run", "Preview proposed changes without applying them").action(async (artifactId, options) => {
32015
+ await jiraSyncCommand(artifactId, options);
32016
+ });
32017
+ jiraCmd.command("statuses [projectKey]").description("Show Jira project statuses and their Marvin status mappings").action(async (projectKey) => {
32018
+ await jiraStatusesCommand(projectKey);
32019
+ });
32020
+ jiraCmd.command("daily").description("Show daily summary of Jira changes with Marvin cross-references").option("--from <date>", "Start date (YYYY-MM-DD, default: today)").option("--to <date>", "End date (YYYY-MM-DD, default: same as --from)").option("--project <key>", "Jira project key (falls back to config)").action(async (options) => {
32021
+ await jiraDailyCommand(options);
32022
+ });
30602
32023
  return program;
30603
32024
  }
30604
32025
  export {