mrvn-cli 0.5.7 → 0.5.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
@@ -22333,7 +22333,7 @@ function poBacklogPage(ctx) {
22333
22333
  }
22334
22334
  }
22335
22335
  }
22336
- const DONE_STATUSES14 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
22336
+ const DONE_STATUSES16 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
22337
22337
  function featureTaskStats(featureId) {
22338
22338
  const fEpics = featureToEpics.get(featureId) ?? [];
22339
22339
  let total = 0;
@@ -22342,7 +22342,7 @@ function poBacklogPage(ctx) {
22342
22342
  for (const epic of fEpics) {
22343
22343
  for (const t of epicToTasks.get(epic.frontmatter.id) ?? []) {
22344
22344
  total++;
22345
- if (DONE_STATUSES14.has(t.frontmatter.status)) done++;
22345
+ if (DONE_STATUSES16.has(t.frontmatter.status)) done++;
22346
22346
  progressSum += getEffectiveProgress(t.frontmatter);
22347
22347
  }
22348
22348
  }
@@ -25141,13 +25141,23 @@ import { tool as tool20 } from "@anthropic-ai/claude-agent-sdk";
25141
25141
  // src/skills/builtin/jira/client.ts
25142
25142
  var JiraClient = class {
25143
25143
  baseUrl;
25144
+ baseUrlV3;
25144
25145
  authHeader;
25145
25146
  constructor(config2) {
25146
- this.baseUrl = `https://${config2.host}/rest/api/2`;
25147
+ const host = config2.host.replace(/^https?:\/\//, "").replace(/\/+$/, "");
25148
+ this.baseUrl = `https://${host}/rest/api/2`;
25149
+ this.baseUrlV3 = `https://${host}/rest/api/3`;
25147
25150
  this.authHeader = "Basic " + Buffer.from(`${config2.email}:${config2.apiToken}`).toString("base64");
25148
25151
  }
25149
25152
  async request(path21, method = "GET", body) {
25150
25153
  const url2 = `${this.baseUrl}${path21}`;
25154
+ return this.doRequest(url2, method, body);
25155
+ }
25156
+ async requestV3(path21, method = "GET", body) {
25157
+ const url2 = `${this.baseUrlV3}${path21}`;
25158
+ return this.doRequest(url2, method, body);
25159
+ }
25160
+ async doRequest(url2, method, body) {
25151
25161
  const headers = {
25152
25162
  Authorization: this.authHeader,
25153
25163
  "Content-Type": "application/json",
@@ -25161,7 +25171,7 @@ var JiraClient = class {
25161
25171
  if (!response.ok) {
25162
25172
  const text = await response.text().catch(() => "");
25163
25173
  throw new Error(
25164
- `Jira API error ${response.status} ${method} ${path21}: ${text}`
25174
+ `Jira API error ${response.status} ${method} ${url2}: ${text}`
25165
25175
  );
25166
25176
  }
25167
25177
  if (response.status === 204) return void 0;
@@ -25174,6 +25184,14 @@ var JiraClient = class {
25174
25184
  });
25175
25185
  return this.request(`/search?${params}`);
25176
25186
  }
25187
+ async searchIssuesV3(jql, fields = ["summary", "status", "issuetype", "priority", "assignee", "labels"], maxResults = 50) {
25188
+ const params = new URLSearchParams({
25189
+ jql,
25190
+ maxResults: String(maxResults),
25191
+ fields: fields.join(",")
25192
+ });
25193
+ return this.requestV3(`/search/jql?${params}`);
25194
+ }
25177
25195
  async getIssue(key) {
25178
25196
  return this.request(`/issue/${encodeURIComponent(key)}`);
25179
25197
  }
@@ -25187,6 +25205,28 @@ var JiraClient = class {
25187
25205
  { fields }
25188
25206
  );
25189
25207
  }
25208
+ async getIssueWithLinks(key) {
25209
+ return this.request(
25210
+ `/issue/${encodeURIComponent(key)}?fields=summary,status,issuetype,priority,assignee,labels,subtasks,issuelinks`
25211
+ );
25212
+ }
25213
+ async getChangelog(key) {
25214
+ const result = await this.request(
25215
+ `/issue/${encodeURIComponent(key)}/changelog?maxResults=100`
25216
+ );
25217
+ return result.values;
25218
+ }
25219
+ async getComments(key) {
25220
+ const result = await this.request(
25221
+ `/issue/${encodeURIComponent(key)}/comment?maxResults=100`
25222
+ );
25223
+ return result.comments;
25224
+ }
25225
+ async getRemoteLinks(key) {
25226
+ return this.request(
25227
+ `/issue/${encodeURIComponent(key)}/remotelink`
25228
+ );
25229
+ }
25190
25230
  async addComment(key, body) {
25191
25231
  await this.request(
25192
25232
  `/issue/${encodeURIComponent(key)}/comment`,
@@ -25200,7 +25240,651 @@ function createJiraClient(jiraUserConfig) {
25200
25240
  const email3 = jiraUserConfig?.email ?? process.env.JIRA_EMAIL;
25201
25241
  const apiToken = jiraUserConfig?.apiToken ?? process.env.JIRA_API_TOKEN;
25202
25242
  if (!host || !email3 || !apiToken) return null;
25203
- return { client: new JiraClient({ host, email: email3, apiToken }), host };
25243
+ const normalizedHost = host.replace(/^https?:\/\//, "").replace(/\/+$/, "");
25244
+ return { client: new JiraClient({ host, email: email3, apiToken }), host: normalizedHost };
25245
+ }
25246
+
25247
+ // src/skills/builtin/jira/sync.ts
25248
+ var DONE_STATUSES14 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "obsolete", "wont do"]);
25249
+ var DEFAULT_ACTION_STATUS_MAP = {
25250
+ done: ["Done", "Closed", "Resolved", "Obsolete", "Wont Do"],
25251
+ "in-progress": ["In Progress", "In Review", "Reviewing", "Testing"],
25252
+ blocked: ["Blocked"],
25253
+ open: ["To Do", "Open", "Backlog", "New"]
25254
+ };
25255
+ var DEFAULT_TASK_STATUS_MAP = {
25256
+ done: ["Done", "Closed", "Resolved", "Obsolete", "Wont Do"],
25257
+ review: ["In Review", "Code Review", "Reviewing", "Testing"],
25258
+ "in-progress": ["In Progress"],
25259
+ ready: ["Ready", "Selected for Development"],
25260
+ blocked: ["Blocked"],
25261
+ backlog: ["To Do", "Open", "Backlog", "New"]
25262
+ };
25263
+ function buildStatusLookup(configMap, defaults) {
25264
+ const map2 = configMap ?? defaults;
25265
+ const lookup = /* @__PURE__ */ new Map();
25266
+ for (const [marvinStatus, jiraStatuses] of Object.entries(map2)) {
25267
+ for (const js of jiraStatuses) {
25268
+ lookup.set(js.toLowerCase(), marvinStatus);
25269
+ }
25270
+ }
25271
+ return lookup;
25272
+ }
25273
+ function mapJiraStatusForAction(status, configMap) {
25274
+ const lookup = buildStatusLookup(configMap, DEFAULT_ACTION_STATUS_MAP);
25275
+ return lookup.get(status.toLowerCase()) ?? "open";
25276
+ }
25277
+ function mapJiraStatusForTask(status, configMap) {
25278
+ const lookup = buildStatusLookup(configMap, DEFAULT_TASK_STATUS_MAP);
25279
+ return lookup.get(status.toLowerCase()) ?? "backlog";
25280
+ }
25281
+ function computeSubtaskProgress(subtasks) {
25282
+ if (subtasks.length === 0) return 0;
25283
+ const done = subtasks.filter(
25284
+ (s) => DONE_STATUSES14.has(s.fields.status.name.toLowerCase())
25285
+ ).length;
25286
+ return Math.round(done / subtasks.length * 100);
25287
+ }
25288
+ async function fetchJiraStatus(store, client, host, artifactId, statusMap) {
25289
+ const result = { artifacts: [], errors: [] };
25290
+ const actions = store.list({ type: "action" });
25291
+ const tasks = store.list({ type: "task" });
25292
+ let candidates = [...actions, ...tasks].filter(
25293
+ (d) => d.frontmatter.jiraKey
25294
+ );
25295
+ if (artifactId) {
25296
+ candidates = candidates.filter((d) => d.frontmatter.id === artifactId);
25297
+ if (candidates.length === 0) {
25298
+ const doc = store.get(artifactId);
25299
+ if (doc) {
25300
+ result.errors.push(
25301
+ `${artifactId} has no jiraKey \u2014 use push_artifact_to_jira or link_to_jira first`
25302
+ );
25303
+ } else {
25304
+ result.errors.push(`Artifact ${artifactId} not found`);
25305
+ }
25306
+ return result;
25307
+ }
25308
+ }
25309
+ candidates = candidates.filter(
25310
+ (d) => !DONE_STATUSES14.has(d.frontmatter.status)
25311
+ );
25312
+ for (const doc of candidates) {
25313
+ const jiraKey = doc.frontmatter.jiraKey;
25314
+ const artifactType = doc.frontmatter.type;
25315
+ try {
25316
+ const issue2 = await client.getIssueWithLinks(jiraKey);
25317
+ const proposedStatus = artifactType === "task" ? mapJiraStatusForTask(issue2.fields.status.name, statusMap?.task) : mapJiraStatusForAction(issue2.fields.status.name, statusMap?.action);
25318
+ const currentStatus = doc.frontmatter.status;
25319
+ const linkedIssues = [];
25320
+ if (issue2.fields.subtasks) {
25321
+ for (const sub of issue2.fields.subtasks) {
25322
+ linkedIssues.push({
25323
+ key: sub.key,
25324
+ summary: sub.fields.summary,
25325
+ status: sub.fields.status.name,
25326
+ relationship: "subtask",
25327
+ isDone: DONE_STATUSES14.has(sub.fields.status.name.toLowerCase())
25328
+ });
25329
+ }
25330
+ }
25331
+ if (issue2.fields.issuelinks) {
25332
+ for (const link of issue2.fields.issuelinks) {
25333
+ if (link.outwardIssue) {
25334
+ linkedIssues.push({
25335
+ key: link.outwardIssue.key,
25336
+ summary: link.outwardIssue.fields.summary,
25337
+ status: link.outwardIssue.fields.status.name,
25338
+ relationship: link.type.outward,
25339
+ isDone: DONE_STATUSES14.has(
25340
+ link.outwardIssue.fields.status.name.toLowerCase()
25341
+ )
25342
+ });
25343
+ }
25344
+ if (link.inwardIssue) {
25345
+ linkedIssues.push({
25346
+ key: link.inwardIssue.key,
25347
+ summary: link.inwardIssue.fields.summary,
25348
+ status: link.inwardIssue.fields.status.name,
25349
+ relationship: link.type.inward,
25350
+ isDone: DONE_STATUSES14.has(
25351
+ link.inwardIssue.fields.status.name.toLowerCase()
25352
+ )
25353
+ });
25354
+ }
25355
+ }
25356
+ }
25357
+ const subtasks = issue2.fields.subtasks ?? [];
25358
+ let proposedProgress;
25359
+ if (subtasks.length > 0 && !doc.frontmatter.progressOverride) {
25360
+ proposedProgress = computeSubtaskProgress(subtasks);
25361
+ }
25362
+ const currentProgress = doc.frontmatter.progress;
25363
+ result.artifacts.push({
25364
+ id: doc.frontmatter.id,
25365
+ type: artifactType,
25366
+ jiraKey,
25367
+ jiraUrl: `https://${host}/browse/${jiraKey}`,
25368
+ jiraSummary: issue2.fields.summary,
25369
+ jiraStatus: issue2.fields.status.name,
25370
+ currentMarvinStatus: currentStatus,
25371
+ proposedMarvinStatus: proposedStatus,
25372
+ statusChanged: currentStatus !== proposedStatus,
25373
+ currentProgress,
25374
+ proposedProgress,
25375
+ progressChanged: proposedProgress !== void 0 && proposedProgress !== currentProgress,
25376
+ linkedIssues
25377
+ });
25378
+ } catch (err) {
25379
+ result.errors.push(
25380
+ `${doc.frontmatter.id} (${jiraKey}): ${err instanceof Error ? err.message : String(err)}`
25381
+ );
25382
+ }
25383
+ }
25384
+ return result;
25385
+ }
25386
+ async function syncJiraProgress(store, client, host, artifactId, statusMap) {
25387
+ const fetchResult = await fetchJiraStatus(store, client, host, artifactId, statusMap);
25388
+ const result = {
25389
+ updated: [],
25390
+ unchanged: 0,
25391
+ errors: [...fetchResult.errors]
25392
+ };
25393
+ for (const artifact of fetchResult.artifacts) {
25394
+ const hasChanges = artifact.statusChanged || artifact.progressChanged || artifact.linkedIssues.length > 0;
25395
+ if (hasChanges) {
25396
+ const updates = {
25397
+ status: artifact.proposedMarvinStatus,
25398
+ lastJiraSyncAt: (/* @__PURE__ */ new Date()).toISOString(),
25399
+ jiraLinkedIssues: artifact.linkedIssues
25400
+ };
25401
+ if (artifact.proposedProgress !== void 0) {
25402
+ updates.progress = artifact.proposedProgress;
25403
+ }
25404
+ store.update(artifact.id, updates);
25405
+ if (artifact.type === "task") {
25406
+ propagateProgressFromTask(store, artifact.id);
25407
+ } else if (artifact.type === "action") {
25408
+ propagateProgressToAction(store, artifact.id);
25409
+ }
25410
+ result.updated.push({
25411
+ id: artifact.id,
25412
+ jiraKey: artifact.jiraKey,
25413
+ oldStatus: artifact.currentMarvinStatus,
25414
+ newStatus: artifact.proposedMarvinStatus,
25415
+ linkedIssues: artifact.linkedIssues
25416
+ });
25417
+ } else {
25418
+ store.update(artifact.id, {
25419
+ lastJiraSyncAt: (/* @__PURE__ */ new Date()).toISOString()
25420
+ });
25421
+ result.unchanged++;
25422
+ }
25423
+ }
25424
+ return result;
25425
+ }
25426
+
25427
+ // src/skills/builtin/jira/daily.ts
25428
+ var BLOCKER_PATTERNS = [
25429
+ /\bblocked\b/i,
25430
+ /\bblocking\b/i,
25431
+ /\bwaiting\s+for\b/i,
25432
+ /\bon\s+hold\b/i,
25433
+ /\bcan'?t\s+proceed\b/i,
25434
+ /\bdepends?\s+on\b/i,
25435
+ /\bstuck\b/i,
25436
+ /\bneed[s]?\s+(to\s+wait|approval|input|clarification)\b/i
25437
+ ];
25438
+ var DECISION_PATTERNS = [
25439
+ /\bdecided\b/i,
25440
+ /\bagreed\b/i,
25441
+ /\bapproved?\b/i,
25442
+ /\blet'?s?\s+go\s+with\b/i,
25443
+ /\bwe('ll|\s+will)\s+(use|go|proceed|adopt)\b/i,
25444
+ /\bsigned\s+off\b/i,
25445
+ /\bconfirmed\b/i
25446
+ ];
25447
+ var QUESTION_PATTERNS = [
25448
+ /\?/,
25449
+ /\bdoes\s+anyone\s+know\b/i,
25450
+ /\bhow\s+should\s+we\b/i,
25451
+ /\bneed\s+clarification\b/i,
25452
+ /\bwhat('s|\s+is)\s+the\s+(plan|approach|status)\b/i,
25453
+ /\bshould\s+we\b/i,
25454
+ /\bany\s+(idea|thought|suggestion)s?\b/i,
25455
+ /\bopen\s+question\b/i
25456
+ ];
25457
+ var RESOLUTION_PATTERNS = [
25458
+ /\bfixed\b/i,
25459
+ /\bresolved\b/i,
25460
+ /\bmerged\b/i,
25461
+ /\bdeployed\b/i,
25462
+ /\bcompleted?\b/i,
25463
+ /\bshipped\b/i,
25464
+ /\bimplemented\b/i,
25465
+ /\bclosed\b/i
25466
+ ];
25467
+ function detectCommentSignals(text) {
25468
+ const signals = [];
25469
+ const lines = text.split("\n");
25470
+ for (const line of lines) {
25471
+ const trimmed = line.trim();
25472
+ if (!trimmed) continue;
25473
+ for (const pattern of BLOCKER_PATTERNS) {
25474
+ if (pattern.test(trimmed)) {
25475
+ signals.push({ type: "blocker", snippet: truncate(trimmed, 120) });
25476
+ break;
25477
+ }
25478
+ }
25479
+ for (const pattern of DECISION_PATTERNS) {
25480
+ if (pattern.test(trimmed)) {
25481
+ signals.push({ type: "decision", snippet: truncate(trimmed, 120) });
25482
+ break;
25483
+ }
25484
+ }
25485
+ for (const pattern of QUESTION_PATTERNS) {
25486
+ if (pattern.test(trimmed)) {
25487
+ signals.push({ type: "question", snippet: truncate(trimmed, 120) });
25488
+ break;
25489
+ }
25490
+ }
25491
+ for (const pattern of RESOLUTION_PATTERNS) {
25492
+ if (pattern.test(trimmed)) {
25493
+ signals.push({ type: "resolution", snippet: truncate(trimmed, 120) });
25494
+ break;
25495
+ }
25496
+ }
25497
+ }
25498
+ const seen = /* @__PURE__ */ new Set();
25499
+ return signals.filter((s) => {
25500
+ if (seen.has(s.type)) return false;
25501
+ seen.add(s.type);
25502
+ return true;
25503
+ });
25504
+ }
25505
+ var STOP_WORDS = /* @__PURE__ */ new Set([
25506
+ "a",
25507
+ "an",
25508
+ "the",
25509
+ "and",
25510
+ "or",
25511
+ "but",
25512
+ "in",
25513
+ "on",
25514
+ "at",
25515
+ "to",
25516
+ "for",
25517
+ "of",
25518
+ "with",
25519
+ "by",
25520
+ "from",
25521
+ "is",
25522
+ "are",
25523
+ "was",
25524
+ "were",
25525
+ "be",
25526
+ "been",
25527
+ "this",
25528
+ "that",
25529
+ "it",
25530
+ "its",
25531
+ "as",
25532
+ "not",
25533
+ "no",
25534
+ "if",
25535
+ "do",
25536
+ "does",
25537
+ "new",
25538
+ "via",
25539
+ "use",
25540
+ "using",
25541
+ "based",
25542
+ "into",
25543
+ "e.g",
25544
+ "etc"
25545
+ ]);
25546
+ function tokenize(text) {
25547
+ return new Set(
25548
+ text.toLowerCase().replace(/[^a-z0-9\s-]/g, " ").split(/[\s-]+/).filter((w) => w.length > 2 && !STOP_WORDS.has(w))
25549
+ );
25550
+ }
25551
+ function computeTitleSimilarity(jiraSummary, artifactTitle) {
25552
+ const jiraTokens = tokenize(jiraSummary);
25553
+ const artifactTokens = tokenize(artifactTitle);
25554
+ if (jiraTokens.size === 0 || artifactTokens.size === 0) {
25555
+ return { score: 0, sharedTerms: [] };
25556
+ }
25557
+ const shared = [];
25558
+ for (const token of jiraTokens) {
25559
+ if (artifactTokens.has(token)) {
25560
+ shared.push(token);
25561
+ }
25562
+ }
25563
+ const union2 = /* @__PURE__ */ new Set([...jiraTokens, ...artifactTokens]);
25564
+ const score = shared.length / union2.size;
25565
+ return { score, sharedTerms: shared };
25566
+ }
25567
+ var LINK_SUGGESTION_THRESHOLD = 0.15;
25568
+ var MAX_LINK_SUGGESTIONS = 3;
25569
+ function findLinkSuggestions(jiraSummary, allDocs) {
25570
+ const suggestions = [];
25571
+ for (const doc of allDocs) {
25572
+ const fm = doc.frontmatter;
25573
+ if (fm.jiraKey) continue;
25574
+ const { score, sharedTerms } = computeTitleSimilarity(
25575
+ jiraSummary,
25576
+ fm.title
25577
+ );
25578
+ if (score >= LINK_SUGGESTION_THRESHOLD && sharedTerms.length >= 2) {
25579
+ suggestions.push({
25580
+ artifactId: fm.id,
25581
+ artifactType: fm.type,
25582
+ artifactTitle: fm.title,
25583
+ score,
25584
+ sharedTerms
25585
+ });
25586
+ }
25587
+ }
25588
+ return suggestions.sort((a, b) => b.score - a.score).slice(0, MAX_LINK_SUGGESTIONS);
25589
+ }
25590
+ function extractCommentText(body) {
25591
+ if (typeof body === "string") return body;
25592
+ if (!body || typeof body !== "object") return "";
25593
+ const parts = [];
25594
+ function walk(node) {
25595
+ if (!node || typeof node !== "object") return;
25596
+ const n = node;
25597
+ if (n.type === "text" && typeof n.text === "string") {
25598
+ parts.push(n.text);
25599
+ }
25600
+ if (Array.isArray(n.content)) {
25601
+ for (const child of n.content) walk(child);
25602
+ }
25603
+ }
25604
+ walk(body);
25605
+ return parts.join(" ");
25606
+ }
25607
+ function truncate(text, maxLen = 200) {
25608
+ if (text.length <= maxLen) return text;
25609
+ return text.slice(0, maxLen) + "\u2026";
25610
+ }
25611
+ function isWithinRange(timestamp, range) {
25612
+ const date5 = timestamp.slice(0, 10);
25613
+ return date5 >= range.from && date5 <= range.to;
25614
+ }
25615
+ function isConfluenceUrl(url2) {
25616
+ return /atlassian\.net\/wiki\//i.test(url2) || /\/confluence\//i.test(url2);
25617
+ }
25618
+ var DONE_STATUSES15 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "obsolete", "wont do"]);
25619
+ async function fetchJiraDaily(store, client, host, projectKey, dateRange, statusMap) {
25620
+ const summary = {
25621
+ dateRange,
25622
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
25623
+ projectKey,
25624
+ issues: [],
25625
+ proposedActions: [],
25626
+ errors: []
25627
+ };
25628
+ const jql = `project = ${projectKey} AND updated >= "${dateRange.from}" AND updated <= "${dateRange.to} 23:59" ORDER BY updated DESC`;
25629
+ let searchResult;
25630
+ try {
25631
+ searchResult = await client.searchIssuesV3(
25632
+ jql,
25633
+ ["summary", "status", "issuetype", "priority", "assignee", "labels"],
25634
+ 100
25635
+ );
25636
+ } catch (err) {
25637
+ summary.errors.push(
25638
+ `Search failed: ${err instanceof Error ? err.message : String(err)}`
25639
+ );
25640
+ return summary;
25641
+ }
25642
+ const allDocs = [
25643
+ ...store.list({ type: "action" }),
25644
+ ...store.list({ type: "task" }),
25645
+ ...store.list({ type: "decision" }),
25646
+ ...store.list({ type: "question" })
25647
+ ];
25648
+ const otherTypes = store.registeredTypes.filter(
25649
+ (t) => !["action", "task", "decision", "question"].includes(t)
25650
+ );
25651
+ for (const t of otherTypes) {
25652
+ allDocs.push(...store.list({ type: t }));
25653
+ }
25654
+ const jiraKeyToArtifacts = /* @__PURE__ */ new Map();
25655
+ for (const doc of allDocs) {
25656
+ const jk = doc.frontmatter.jiraKey;
25657
+ if (jk) {
25658
+ const list = jiraKeyToArtifacts.get(jk) ?? [];
25659
+ list.push(doc);
25660
+ jiraKeyToArtifacts.set(jk, list);
25661
+ }
25662
+ }
25663
+ const BATCH_SIZE = 5;
25664
+ const issues = searchResult.issues;
25665
+ for (let i = 0; i < issues.length; i += BATCH_SIZE) {
25666
+ const batch = issues.slice(i, i + BATCH_SIZE);
25667
+ const results = await Promise.allSettled(
25668
+ batch.map(
25669
+ (issue2) => processIssue(issue2, client, host, dateRange, jiraKeyToArtifacts, allDocs, statusMap)
25670
+ )
25671
+ );
25672
+ for (let j = 0; j < results.length; j++) {
25673
+ const r = results[j];
25674
+ if (r.status === "fulfilled") {
25675
+ summary.issues.push(r.value);
25676
+ } else {
25677
+ summary.errors.push(
25678
+ `${batch[j].key}: ${r.reason instanceof Error ? r.reason.message : String(r.reason)}`
25679
+ );
25680
+ }
25681
+ }
25682
+ }
25683
+ summary.proposedActions = generateProposedActions(summary.issues);
25684
+ return summary;
25685
+ }
25686
+ async function processIssue(issue2, client, host, dateRange, jiraKeyToArtifacts, allDocs, statusMap) {
25687
+ const [changelogResult, commentsResult, remoteLinksResult, issueWithLinks] = await Promise.all([
25688
+ client.getChangelog(issue2.key).catch(() => []),
25689
+ client.getComments(issue2.key).catch(() => []),
25690
+ client.getRemoteLinks(issue2.key).catch(() => []),
25691
+ client.getIssueWithLinks(issue2.key).catch(() => null)
25692
+ ]);
25693
+ const changes = [];
25694
+ for (const entry of changelogResult) {
25695
+ if (!isWithinRange(entry.created, dateRange)) continue;
25696
+ for (const item of entry.items) {
25697
+ changes.push({
25698
+ field: item.field,
25699
+ from: item.fromString,
25700
+ to: item.toString,
25701
+ author: entry.author.displayName,
25702
+ timestamp: entry.created
25703
+ });
25704
+ }
25705
+ }
25706
+ const comments = [];
25707
+ for (const comment of commentsResult) {
25708
+ if (!isWithinRange(comment.created, dateRange) && !isWithinRange(comment.updated, dateRange)) {
25709
+ continue;
25710
+ }
25711
+ const fullText = extractCommentText(comment.body);
25712
+ const signals = detectCommentSignals(fullText);
25713
+ comments.push({
25714
+ author: comment.author.displayName,
25715
+ created: comment.created,
25716
+ bodyPreview: truncate(fullText),
25717
+ signals
25718
+ });
25719
+ }
25720
+ const confluenceLinks = [];
25721
+ for (const rl of remoteLinksResult) {
25722
+ if (isConfluenceUrl(rl.object.url)) {
25723
+ confluenceLinks.push({
25724
+ url: rl.object.url,
25725
+ title: rl.object.title
25726
+ });
25727
+ }
25728
+ }
25729
+ const linkedIssues = [];
25730
+ if (issueWithLinks) {
25731
+ if (issueWithLinks.fields.subtasks) {
25732
+ for (const sub of issueWithLinks.fields.subtasks) {
25733
+ linkedIssues.push({
25734
+ key: sub.key,
25735
+ summary: sub.fields.summary,
25736
+ status: sub.fields.status.name,
25737
+ relationship: "subtask",
25738
+ isDone: DONE_STATUSES15.has(sub.fields.status.name.toLowerCase())
25739
+ });
25740
+ }
25741
+ }
25742
+ if (issueWithLinks.fields.issuelinks) {
25743
+ for (const link of issueWithLinks.fields.issuelinks) {
25744
+ if (link.outwardIssue) {
25745
+ linkedIssues.push({
25746
+ key: link.outwardIssue.key,
25747
+ summary: link.outwardIssue.fields.summary,
25748
+ status: link.outwardIssue.fields.status.name,
25749
+ relationship: link.type.outward,
25750
+ isDone: DONE_STATUSES15.has(link.outwardIssue.fields.status.name.toLowerCase())
25751
+ });
25752
+ }
25753
+ if (link.inwardIssue) {
25754
+ linkedIssues.push({
25755
+ key: link.inwardIssue.key,
25756
+ summary: link.inwardIssue.fields.summary,
25757
+ status: link.inwardIssue.fields.status.name,
25758
+ relationship: link.type.inward,
25759
+ isDone: DONE_STATUSES15.has(link.inwardIssue.fields.status.name.toLowerCase())
25760
+ });
25761
+ }
25762
+ }
25763
+ }
25764
+ }
25765
+ const marvinArtifacts = [];
25766
+ const artifacts = jiraKeyToArtifacts.get(issue2.key) ?? [];
25767
+ for (const doc of artifacts) {
25768
+ const fm = doc.frontmatter;
25769
+ const artifactType = fm.type;
25770
+ let proposedStatus = null;
25771
+ if (artifactType === "action" || artifactType === "task") {
25772
+ const jiraStatus = issue2.fields.status?.name;
25773
+ if (jiraStatus) {
25774
+ proposedStatus = artifactType === "task" ? mapJiraStatusForTask(jiraStatus, statusMap?.task) : mapJiraStatusForAction(jiraStatus, statusMap?.action);
25775
+ }
25776
+ }
25777
+ marvinArtifacts.push({
25778
+ id: fm.id,
25779
+ type: artifactType,
25780
+ title: fm.title,
25781
+ currentStatus: fm.status,
25782
+ proposedStatus,
25783
+ statusDrift: proposedStatus !== null && proposedStatus !== fm.status
25784
+ });
25785
+ }
25786
+ const linkSuggestions = marvinArtifacts.length === 0 ? findLinkSuggestions(issue2.fields.summary, allDocs) : [];
25787
+ return {
25788
+ key: issue2.key,
25789
+ summary: issue2.fields.summary,
25790
+ currentStatus: issue2.fields.status?.name ?? "Unknown",
25791
+ issueType: issue2.fields.issuetype?.name ?? "Unknown",
25792
+ assignee: issue2.fields.assignee?.displayName ?? null,
25793
+ changes,
25794
+ comments,
25795
+ linkedIssues,
25796
+ confluenceLinks,
25797
+ marvinArtifacts,
25798
+ linkSuggestions
25799
+ };
25800
+ }
25801
+ function generateProposedActions(issues) {
25802
+ const actions = [];
25803
+ for (const issue2 of issues) {
25804
+ for (const artifact of issue2.marvinArtifacts) {
25805
+ if (artifact.statusDrift && artifact.proposedStatus) {
25806
+ actions.push({
25807
+ type: "status-update",
25808
+ description: `Update ${artifact.id} (${artifact.type}) status: ${artifact.currentStatus} \u2192 ${artifact.proposedStatus} (Jira ${issue2.key} is "${issue2.currentStatus}")`,
25809
+ artifactId: artifact.id,
25810
+ jiraKey: issue2.key
25811
+ });
25812
+ }
25813
+ }
25814
+ if (issue2.marvinArtifacts.length === 0 && (issue2.changes.length > 0 || issue2.comments.length > 0)) {
25815
+ actions.push({
25816
+ type: "unlinked-issue",
25817
+ description: `${issue2.key} ("${issue2.summary}") has activity but no Marvin artifact \u2014 consider linking or creating one`,
25818
+ jiraKey: issue2.key
25819
+ });
25820
+ }
25821
+ for (const suggestion of issue2.linkSuggestions) {
25822
+ actions.push({
25823
+ type: "link-suggestion",
25824
+ description: `${issue2.key} ("${issue2.summary}") may match ${suggestion.artifactId} ("${suggestion.artifactTitle}") \u2014 shared terms: ${suggestion.sharedTerms.join(", ")} (${Math.round(suggestion.score * 100)}% similarity)`,
25825
+ artifactId: suggestion.artifactId,
25826
+ jiraKey: issue2.key
25827
+ });
25828
+ }
25829
+ for (const comment of issue2.comments) {
25830
+ for (const signal of comment.signals) {
25831
+ if (signal.type === "blocker") {
25832
+ actions.push({
25833
+ type: "blocker-detected",
25834
+ description: `Blocker in ${issue2.key} comment by ${comment.author}: "${signal.snippet}"`,
25835
+ jiraKey: issue2.key
25836
+ });
25837
+ }
25838
+ if (signal.type === "decision") {
25839
+ actions.push({
25840
+ type: "decision-candidate",
25841
+ description: `Possible decision in ${issue2.key} comment by ${comment.author}: "${signal.snippet}" \u2014 consider creating a decision artifact`,
25842
+ jiraKey: issue2.key
25843
+ });
25844
+ }
25845
+ if (signal.type === "question") {
25846
+ const linkedQuestion = issue2.marvinArtifacts.find(
25847
+ (a) => a.type === "question" && a.currentStatus !== "answered"
25848
+ );
25849
+ if (linkedQuestion) {
25850
+ actions.push({
25851
+ type: "question-candidate",
25852
+ description: `Question in ${issue2.key} comment by ${comment.author} \u2014 may relate to ${linkedQuestion.id} ("${linkedQuestion.title}"): "${signal.snippet}"`,
25853
+ artifactId: linkedQuestion.id,
25854
+ jiraKey: issue2.key
25855
+ });
25856
+ } else {
25857
+ actions.push({
25858
+ type: "question-candidate",
25859
+ description: `Question in ${issue2.key} comment by ${comment.author}: "${signal.snippet}" \u2014 consider creating a question artifact`,
25860
+ jiraKey: issue2.key
25861
+ });
25862
+ }
25863
+ }
25864
+ if (signal.type === "resolution") {
25865
+ const linkedQuestion = issue2.marvinArtifacts.find(
25866
+ (a) => a.type === "question" && a.currentStatus !== "answered"
25867
+ );
25868
+ if (linkedQuestion) {
25869
+ actions.push({
25870
+ type: "resolution-detected",
25871
+ description: `Resolution in ${issue2.key} by ${comment.author} may answer ${linkedQuestion.id} ("${linkedQuestion.title}"): "${signal.snippet}"`,
25872
+ artifactId: linkedQuestion.id,
25873
+ jiraKey: issue2.key
25874
+ });
25875
+ }
25876
+ }
25877
+ }
25878
+ }
25879
+ for (const cl of issue2.confluenceLinks) {
25880
+ actions.push({
25881
+ type: "confluence-review",
25882
+ description: `Confluence page "${cl.title}" linked from ${issue2.key} \u2014 review for relevant updates`,
25883
+ jiraKey: issue2.key
25884
+ });
25885
+ }
25886
+ }
25887
+ return actions;
25204
25888
  }
25205
25889
 
25206
25890
  // src/skills/builtin/jira/tools.ts
@@ -25244,6 +25928,7 @@ function findByJiraKey(store, jiraKey) {
25244
25928
  function createJiraTools(store, projectConfig) {
25245
25929
  const jiraUserConfig = loadUserConfig().jira;
25246
25930
  const defaultProjectKey = projectConfig?.jira?.projectKey;
25931
+ const statusMap = projectConfig?.jira?.statusMap;
25247
25932
  return [
25248
25933
  // --- Local read tools ---
25249
25934
  tool20(
@@ -25404,9 +26089,9 @@ function createJiraTools(store, projectConfig) {
25404
26089
  // --- Local → Jira tools ---
25405
26090
  tool20(
25406
26091
  "push_artifact_to_jira",
25407
- "Create a Jira issue from any Marvin artifact (D/A/Q/F/E) and create a tracking JI-xxx document",
26092
+ "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.",
25408
26093
  {
25409
- artifactId: external_exports.string().describe("Marvin artifact ID (e.g. 'D-001', 'F-003', 'E-002')"),
26094
+ artifactId: external_exports.string().describe("Marvin artifact ID (e.g. 'D-001', 'A-003', 'T-002')"),
25410
26095
  projectKey: external_exports.string().optional().describe("Jira project key (e.g. 'PROJ'). Falls back to jira.projectKey from .marvin/config.yaml if not provided."),
25411
26096
  issueType: external_exports.enum(["Story", "Task", "Bug", "Epic"]).optional().describe("Jira issue type (default: 'Task')")
25412
26097
  },
@@ -25447,6 +26132,24 @@ function createJiraTools(store, projectConfig) {
25447
26132
  description,
25448
26133
  issuetype: { name: args.issueType ?? "Task" }
25449
26134
  });
26135
+ const isDirectLink = artifact.frontmatter.type === "action" || artifact.frontmatter.type === "task";
26136
+ if (isDirectLink) {
26137
+ const existingTags = artifact.frontmatter.tags ?? [];
26138
+ store.update(args.artifactId, {
26139
+ jiraKey: jiraResult.key,
26140
+ jiraUrl: `https://${jira.host}/browse/${jiraResult.key}`,
26141
+ lastJiraSyncAt: (/* @__PURE__ */ new Date()).toISOString(),
26142
+ tags: [...existingTags.filter((t) => !t.startsWith("jira:")), `jira:${jiraResult.key}`]
26143
+ });
26144
+ return {
26145
+ content: [
26146
+ {
26147
+ type: "text",
26148
+ text: `Created Jira ${jiraResult.key} from ${args.artifactId}. Linked directly on the artifact.`
26149
+ }
26150
+ ]
26151
+ };
26152
+ }
25450
26153
  const jiDoc = store.create(
25451
26154
  JIRA_TYPE,
25452
26155
  {
@@ -25568,65 +26271,430 @@ function createJiraTools(store, projectConfig) {
25568
26271
  ]
25569
26272
  };
25570
26273
  }
26274
+ ),
26275
+ // --- Direct Jira linking for actions/tasks ---
26276
+ tool20(
26277
+ "link_to_jira",
26278
+ "Link an existing Jira issue to a Marvin action or task (sets jiraKey directly on the artifact)",
26279
+ {
26280
+ artifactId: external_exports.string().describe("Marvin artifact ID (e.g. 'A-001', 'T-003')"),
26281
+ jiraKey: external_exports.string().describe("Jira issue key (e.g. 'PROJ-123')")
26282
+ },
26283
+ async (args) => {
26284
+ const jira = createJiraClient(jiraUserConfig);
26285
+ if (!jira) return jiraNotConfiguredError();
26286
+ const artifact = store.get(args.artifactId);
26287
+ if (!artifact) {
26288
+ return {
26289
+ content: [
26290
+ { type: "text", text: `Artifact ${args.artifactId} not found` }
26291
+ ],
26292
+ isError: true
26293
+ };
26294
+ }
26295
+ if (artifact.frontmatter.type !== "action" && artifact.frontmatter.type !== "task") {
26296
+ return {
26297
+ content: [
26298
+ {
26299
+ type: "text",
26300
+ 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.`
26301
+ }
26302
+ ],
26303
+ isError: true
26304
+ };
26305
+ }
26306
+ const issue2 = await jira.client.getIssue(args.jiraKey);
26307
+ const existingTags = artifact.frontmatter.tags ?? [];
26308
+ store.update(args.artifactId, {
26309
+ jiraKey: args.jiraKey,
26310
+ jiraUrl: `https://${jira.host}/browse/${args.jiraKey}`,
26311
+ lastJiraSyncAt: (/* @__PURE__ */ new Date()).toISOString(),
26312
+ tags: [...existingTags.filter((t) => !t.startsWith("jira:")), `jira:${args.jiraKey}`]
26313
+ });
26314
+ return {
26315
+ content: [
26316
+ {
26317
+ type: "text",
26318
+ text: `Linked ${args.artifactId} to Jira ${args.jiraKey} ("${issue2.fields.summary}").`
26319
+ }
26320
+ ]
26321
+ };
26322
+ }
26323
+ ),
26324
+ // --- Jira status fetch (read-only) ---
26325
+ tool20(
26326
+ "fetch_jira_status",
26327
+ "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.",
26328
+ {
26329
+ artifactId: external_exports.string().optional().describe("Specific artifact ID to check, or omit to check all Jira-linked actions/tasks")
26330
+ },
26331
+ async (args) => {
26332
+ const jira = createJiraClient(jiraUserConfig);
26333
+ if (!jira) return jiraNotConfiguredError();
26334
+ const fetchResult = await fetchJiraStatus(
26335
+ store,
26336
+ jira.client,
26337
+ jira.host,
26338
+ args.artifactId,
26339
+ statusMap
26340
+ );
26341
+ const parts = [];
26342
+ if (fetchResult.artifacts.length > 0) {
26343
+ for (const a of fetchResult.artifacts) {
26344
+ const changes = [];
26345
+ if (a.statusChanged) {
26346
+ changes.push(`status: ${a.currentMarvinStatus} \u2192 ${a.proposedMarvinStatus}`);
26347
+ }
26348
+ if (a.progressChanged) {
26349
+ changes.push(`progress: ${a.currentProgress ?? 0}% \u2192 ${a.proposedProgress}%`);
26350
+ }
26351
+ const header = `${a.id} (${a.jiraKey}) \u2014 Jira: "${a.jiraSummary}" [${a.jiraStatus}]`;
26352
+ if (changes.length > 0) {
26353
+ parts.push(`${header}
26354
+ Proposed changes: ${changes.join(", ")}`);
26355
+ } else {
26356
+ parts.push(`${header}
26357
+ No status/progress changes.`);
26358
+ }
26359
+ if (a.linkedIssues.length > 0) {
26360
+ const done = a.linkedIssues.filter((l) => l.isDone).length;
26361
+ parts.push(` Linked issues (${done}/${a.linkedIssues.length} done):`);
26362
+ for (const li of a.linkedIssues) {
26363
+ const icon = li.isDone ? "\u2713" : "\u25CB";
26364
+ parts.push(` ${icon} ${li.key} ${li.summary} [${li.relationship}] \u2014 ${li.status}`);
26365
+ }
26366
+ }
26367
+ }
26368
+ parts.push("");
26369
+ parts.push("This is a read-only preview. Use update_action or update_task to apply the proposed status/progress changes.");
26370
+ }
26371
+ if (fetchResult.errors.length > 0) {
26372
+ parts.push("Errors:");
26373
+ for (const err of fetchResult.errors) {
26374
+ parts.push(` ${err}`);
26375
+ }
26376
+ }
26377
+ if (fetchResult.artifacts.length === 0 && fetchResult.errors.length === 0) {
26378
+ parts.push("No Jira-linked actions/tasks found.");
26379
+ }
26380
+ return {
26381
+ content: [{ type: "text", text: parts.join("\n") }],
26382
+ isError: fetchResult.errors.length > 0 && fetchResult.artifacts.length === 0
26383
+ };
26384
+ },
26385
+ { annotations: { readOnlyHint: true } }
26386
+ ),
26387
+ // --- Jira status discovery ---
26388
+ tool20(
26389
+ "fetch_jira_statuses",
26390
+ "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.",
26391
+ {
26392
+ projectKey: external_exports.string().optional().describe("Jira project key (e.g. 'MCB1'). Falls back to jira.projectKey from config."),
26393
+ maxResults: external_exports.number().optional().describe("Max issues to scan (default 100)")
26394
+ },
26395
+ async (args) => {
26396
+ const resolvedProjectKey = args.projectKey ?? defaultProjectKey;
26397
+ if (!resolvedProjectKey) {
26398
+ return {
26399
+ content: [
26400
+ {
26401
+ type: "text",
26402
+ text: "No projectKey provided and no default configured."
26403
+ }
26404
+ ],
26405
+ isError: true
26406
+ };
26407
+ }
26408
+ const jira = createJiraClient(jiraUserConfig);
26409
+ if (!jira) return jiraNotConfiguredError();
26410
+ const host = jira.host;
26411
+ const auth = "Basic " + Buffer.from(
26412
+ `${jiraUserConfig?.email ?? process.env.JIRA_EMAIL}:${jiraUserConfig?.apiToken ?? process.env.JIRA_API_TOKEN}`
26413
+ ).toString("base64");
26414
+ const params = new URLSearchParams({
26415
+ jql: `project = ${resolvedProjectKey}`,
26416
+ maxResults: String(args.maxResults ?? 100),
26417
+ fields: "status"
26418
+ });
26419
+ const resp = await fetch(`https://${host}/rest/api/3/search/jql?${params}`, {
26420
+ headers: { Authorization: auth, Accept: "application/json" }
26421
+ });
26422
+ if (!resp.ok) {
26423
+ const text = await resp.text().catch(() => "");
26424
+ return {
26425
+ content: [
26426
+ {
26427
+ type: "text",
26428
+ text: `Jira API error ${resp.status}: ${text}`
26429
+ }
26430
+ ],
26431
+ isError: true
26432
+ };
26433
+ }
26434
+ const data = await resp.json();
26435
+ const statusCounts = /* @__PURE__ */ new Map();
26436
+ for (const issue2 of data.issues) {
26437
+ const s = issue2.fields.status.name;
26438
+ statusCounts.set(s, (statusCounts.get(s) ?? 0) + 1);
26439
+ }
26440
+ const actionMap = statusMap?.action ?? DEFAULT_ACTION_STATUS_MAP;
26441
+ const taskMap = statusMap?.task ?? DEFAULT_TASK_STATUS_MAP;
26442
+ const actionLookup = /* @__PURE__ */ new Map();
26443
+ for (const [marvin, jiraStatuses] of Object.entries(actionMap)) {
26444
+ for (const js of jiraStatuses) actionLookup.set(js.toLowerCase(), marvin);
26445
+ }
26446
+ const taskLookup = /* @__PURE__ */ new Map();
26447
+ for (const [marvin, jiraStatuses] of Object.entries(taskMap)) {
26448
+ for (const js of jiraStatuses) taskLookup.set(js.toLowerCase(), marvin);
26449
+ }
26450
+ const parts = [
26451
+ `Found ${statusCounts.size} distinct statuses in ${resolvedProjectKey} (scanned ${data.issues.length} of ${data.total} issues):`,
26452
+ ""
26453
+ ];
26454
+ const sorted = [...statusCounts.entries()].sort((a, b) => b[1] - a[1]);
26455
+ const unmappedAction = [];
26456
+ const unmappedTask = [];
26457
+ for (const [status, count] of sorted) {
26458
+ const actionTarget = actionLookup.get(status.toLowerCase());
26459
+ const taskTarget = taskLookup.get(status.toLowerCase());
26460
+ const actionLabel = actionTarget ? `\u2192 ${actionTarget}` : "UNMAPPED (\u2192 open)";
26461
+ const taskLabel = taskTarget ? `\u2192 ${taskTarget}` : "UNMAPPED (\u2192 backlog)";
26462
+ parts.push(` ${status} (${count} issues)`);
26463
+ parts.push(` action: ${actionLabel}`);
26464
+ parts.push(` task: ${taskLabel}`);
26465
+ if (!actionTarget) unmappedAction.push(status);
26466
+ if (!taskTarget) unmappedTask.push(status);
26467
+ }
26468
+ if (unmappedAction.length > 0 || unmappedTask.length > 0) {
26469
+ parts.push("");
26470
+ parts.push("To fix unmapped statuses, add jira.statusMap to .marvin/config.yaml:");
26471
+ parts.push(" jira:");
26472
+ parts.push(" statusMap:");
26473
+ if (unmappedAction.length > 0) {
26474
+ parts.push(" action:");
26475
+ parts.push(` # Map these: ${unmappedAction.join(", ")}`);
26476
+ parts.push(" # <marvin-status>: [<jira-status>, ...]");
26477
+ }
26478
+ if (unmappedTask.length > 0) {
26479
+ parts.push(" task:");
26480
+ parts.push(` # Map these: ${unmappedTask.join(", ")}`);
26481
+ parts.push(" # <marvin-status>: [<jira-status>, ...]");
26482
+ }
26483
+ } else {
26484
+ parts.push("");
26485
+ parts.push("All statuses are mapped.");
26486
+ }
26487
+ const usingConfig = statusMap?.action || statusMap?.task;
26488
+ parts.push("");
26489
+ parts.push(usingConfig ? "Using status maps from .marvin/config.yaml." : "Using built-in default status maps (no jira.statusMap in config).");
26490
+ return {
26491
+ content: [{ type: "text", text: parts.join("\n") }]
26492
+ };
26493
+ },
26494
+ { annotations: { readOnlyHint: true } }
26495
+ ),
26496
+ // --- Jira daily summary ---
26497
+ tool20(
26498
+ "fetch_jira_daily",
26499
+ "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.",
26500
+ {
26501
+ from: external_exports.string().optional().describe("Start date (YYYY-MM-DD). Defaults to today."),
26502
+ to: external_exports.string().optional().describe("End date (YYYY-MM-DD). Defaults to same as 'from'."),
26503
+ projectKey: external_exports.string().optional().describe("Jira project key. Falls back to jira.projectKey from config.")
26504
+ },
26505
+ async (args) => {
26506
+ const resolvedProjectKey = args.projectKey ?? defaultProjectKey;
26507
+ if (!resolvedProjectKey) {
26508
+ return {
26509
+ content: [
26510
+ {
26511
+ type: "text",
26512
+ text: "No projectKey provided and no default configured."
26513
+ }
26514
+ ],
26515
+ isError: true
26516
+ };
26517
+ }
26518
+ const jira = createJiraClient(jiraUserConfig);
26519
+ if (!jira) return jiraNotConfiguredError();
26520
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
26521
+ const fromDate = args.from ?? today;
26522
+ const toDate = args.to ?? fromDate;
26523
+ const daily = await fetchJiraDaily(
26524
+ store,
26525
+ jira.client,
26526
+ jira.host,
26527
+ resolvedProjectKey,
26528
+ { from: fromDate, to: toDate },
26529
+ statusMap
26530
+ );
26531
+ return {
26532
+ content: [{ type: "text", text: formatDailySummary(daily) }],
26533
+ isError: daily.errors.length > 0 && daily.issues.length === 0
26534
+ };
26535
+ },
26536
+ { annotations: { readOnlyHint: true } }
25571
26537
  )
25572
26538
  ];
25573
26539
  }
26540
+ function formatDailySummary(daily) {
26541
+ const parts = [];
26542
+ const rangeLabel = daily.dateRange.from === daily.dateRange.to ? daily.dateRange.from : `${daily.dateRange.from} to ${daily.dateRange.to}`;
26543
+ parts.push(`Jira Daily Summary \u2014 ${daily.projectKey} \u2014 ${rangeLabel}`);
26544
+ parts.push(`${daily.issues.length} issue(s) updated.
26545
+ `);
26546
+ const linked = daily.issues.filter((i) => i.marvinArtifacts.length > 0);
26547
+ const unlinked = daily.issues.filter((i) => i.marvinArtifacts.length === 0);
26548
+ if (linked.length > 0) {
26549
+ parts.push("## Linked Issues (with Marvin artifacts)\n");
26550
+ for (const issue2 of linked) {
26551
+ parts.push(formatIssueEntry(issue2));
26552
+ }
26553
+ }
26554
+ if (unlinked.length > 0) {
26555
+ parts.push("## Unlinked Issues (no Marvin artifact)\n");
26556
+ for (const issue2 of unlinked) {
26557
+ parts.push(formatIssueEntry(issue2));
26558
+ }
26559
+ }
26560
+ if (daily.proposedActions.length > 0) {
26561
+ parts.push("## Proposed Actions\n");
26562
+ for (const action of daily.proposedActions) {
26563
+ 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}";
26564
+ parts.push(` ${icon} ${action.description}`);
26565
+ }
26566
+ parts.push("");
26567
+ parts.push("These are suggestions. Use update_action, update_task, or other tools to apply changes.");
26568
+ }
26569
+ if (daily.errors.length > 0) {
26570
+ parts.push("\n## Errors\n");
26571
+ for (const err of daily.errors) {
26572
+ parts.push(` ${err}`);
26573
+ }
26574
+ }
26575
+ return parts.join("\n");
26576
+ }
26577
+ function formatIssueEntry(issue2) {
26578
+ const lines = [];
26579
+ const artifacts = issue2.marvinArtifacts.map((a) => a.id).join(", ");
26580
+ const artifactLabel = artifacts ? ` \u2192 ${artifacts}` : "";
26581
+ lines.push(`### ${issue2.key} \u2014 ${issue2.summary} [${issue2.currentStatus}]${artifactLabel}`);
26582
+ lines.push(` Type: ${issue2.issueType} | Assignee: ${issue2.assignee ?? "unassigned"}`);
26583
+ for (const a of issue2.marvinArtifacts) {
26584
+ if (a.statusDrift) {
26585
+ lines.push(` \u26A0 ${a.id} status drift: Marvin="${a.currentStatus}" vs proposed="${a.proposedStatus}"`);
26586
+ }
26587
+ }
26588
+ if (issue2.changes.length > 0) {
26589
+ lines.push(" Changes:");
26590
+ for (const c of issue2.changes) {
26591
+ lines.push(` ${c.field}: ${c.from ?? "\u2205"} \u2192 ${c.to ?? "\u2205"} (${c.author}, ${c.timestamp.slice(0, 16)})`);
26592
+ }
26593
+ }
26594
+ if (issue2.comments.length > 0) {
26595
+ lines.push(` Comments (${issue2.comments.length}):`);
26596
+ for (const c of issue2.comments) {
26597
+ let signalIcons = "";
26598
+ if (c.signals.length > 0) {
26599
+ const icons = c.signals.map(
26600
+ (s) => s.type === "blocker" ? "\u{1F6AB}" : s.type === "decision" ? "\u2696" : s.type === "question" ? "?" : "\u2713"
26601
+ );
26602
+ signalIcons = ` [${icons.join("")}]`;
26603
+ }
26604
+ lines.push(` ${c.author} (${c.created.slice(0, 16)})${signalIcons}: ${c.bodyPreview}`);
26605
+ }
26606
+ }
26607
+ if (issue2.linkSuggestions.length > 0) {
26608
+ lines.push(" Possible Marvin matches:");
26609
+ for (const s of issue2.linkSuggestions) {
26610
+ lines.push(` \u{1F517} ${s.artifactId} ("${s.artifactTitle}") \u2014 ${Math.round(s.score * 100)}% match [${s.sharedTerms.join(", ")}]`);
26611
+ }
26612
+ }
26613
+ if (issue2.linkedIssues.length > 0) {
26614
+ lines.push(" Linked issues:");
26615
+ for (const li of issue2.linkedIssues) {
26616
+ const icon = li.isDone ? "\u2713" : "\u25CB";
26617
+ lines.push(` ${icon} ${li.key} ${li.summary} [${li.relationship}] \u2014 ${li.status}`);
26618
+ }
26619
+ }
26620
+ if (issue2.confluenceLinks.length > 0) {
26621
+ lines.push(" Confluence pages:");
26622
+ for (const cl of issue2.confluenceLinks) {
26623
+ lines.push(` \u{1F4C4} ${cl.title}: ${cl.url}`);
26624
+ }
26625
+ }
26626
+ lines.push("");
26627
+ return lines.join("\n");
26628
+ }
25574
26629
 
25575
26630
  // src/skills/builtin/jira/index.ts
26631
+ var COMMON_TOOLS = `**Available tools:**
26632
+ - \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues (JI-xxx documents)
26633
+ - \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import issues from Jira by key or JQL query
26634
+ - \`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.
26635
+ - \`link_to_jira\` \u2014 link an existing Jira issue to a Marvin action or task (sets \`jiraKey\` directly on the artifact)
26636
+ - \`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.
26637
+ - \`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).
26638
+ - \`fetch_jira_statuses\` \u2014 **read-only**: discover all Jira statuses in a project and show their Marvin mappings (mapped vs unmapped).
26639
+ - \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
26640
+ - \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx`;
26641
+ var COMMON_WORKFLOW = `**Jira sync workflow:**
26642
+ 1. Call \`fetch_jira_status\` to see what Jira reports for linked artifacts
26643
+ 2. Analyze the proposed changes (status transitions, subtask progress, blockers from linked issues)
26644
+ 3. Use \`update_action\` / \`update_task\` to apply the changes you agree with
26645
+
26646
+ **Daily review workflow:**
26647
+ 1. Call \`fetch_jira_daily\` (optionally with \`from\`/\`to\` date range) to get a summary of all Jira activity
26648
+ 2. Review the proposed actions: status updates, unlinked issues to track, questions that may be answered, Confluence pages to review
26649
+ 3. Use existing tools to apply changes, create new artifacts, or link untracked issues`;
25576
26650
  var jiraSkill = {
25577
26651
  id: "jira",
25578
26652
  name: "Jira Integration",
25579
26653
  description: "Bidirectional sync between Marvin artifacts and Jira issues",
25580
26654
  version: "1.0.0",
25581
26655
  format: "builtin-ts",
25582
- // No default persona affinity — opt-in via config.yaml skills section
25583
26656
  documentTypeRegistrations: [
25584
26657
  { type: "jira-issue", dirName: "jira-issues", idPrefix: "JI" }
25585
26658
  ],
25586
26659
  tools: (store, projectConfig) => createJiraTools(store, projectConfig),
25587
26660
  promptFragments: {
25588
- "product-owner": `You have the **Jira Integration** skill. You can pull issues from Jira and push Marvin artifacts to Jira.
26661
+ "product-owner": `You have the **Jira Integration** skill.
25589
26662
 
25590
- **Available tools:**
25591
- - \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues
25592
- - \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import issues from Jira by key or JQL query
25593
- - \`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\`.
25594
- - \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
25595
- - \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx
26663
+ ${COMMON_TOOLS}
26664
+
26665
+ ${COMMON_WORKFLOW}
25596
26666
 
25597
26667
  **As Product Owner, use Jira integration to:**
26668
+ - Use \`fetch_jira_daily\` for daily standups \u2014 review what changed, identify status drift, spot untracked work
25598
26669
  - Pull stakeholder-reported issues for triage and prioritization
25599
26670
  - Push approved features as Stories for development tracking
25600
26671
  - Link decisions to Jira issues for audit trail and traceability
25601
- - Use JQL queries to review backlog status (e.g. \`project = PROJ AND status = "To Do"\`)`,
25602
- "tech-lead": `You have the **Jira Integration** skill. You can pull issues from Jira and push Marvin artifacts to Jira.
26672
+ - Use \`fetch_jira_statuses\` when setting up a new project to configure status mappings`,
26673
+ "tech-lead": `You have the **Jira Integration** skill.
25603
26674
 
25604
- **Available tools:**
25605
- - \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues
25606
- - \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import issues from Jira by key or JQL query
25607
- - \`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\`.
25608
- - \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
25609
- - \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx
26675
+ ${COMMON_TOOLS}
26676
+
26677
+ ${COMMON_WORKFLOW}
25610
26678
 
25611
26679
  **As Tech Lead, use Jira integration to:**
26680
+ - Use \`fetch_jira_daily\` to review technical progress \u2014 status transitions, new comments, Confluence design docs
25612
26681
  - Pull technical issues and bugs for sprint planning and estimation
25613
26682
  - Push epics, tasks, and technical decisions to Jira for cross-team visibility
25614
- - Bidirectional sync to keep local governance and Jira in alignment
25615
- - Use JQL queries to track technical debt (e.g. \`labels = "tech-debt" AND status != "Done"\`)`,
25616
- "delivery-manager": `You have the **Jira Integration** skill. You can pull issues from Jira and push Marvin artifacts to Jira.
26683
+ - Use \`link_to_jira\` to connect Marvin tasks to existing Jira tickets
26684
+ - Use \`fetch_jira_statuses\` to verify status mappings match the team's Jira workflow`,
26685
+ "delivery-manager": `You have the **Jira Integration** skill.
25617
26686
 
25618
- **Available tools:**
25619
- - \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues
25620
- - \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import issues from Jira by key or JQL query
25621
- - \`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\`.
25622
- - \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
25623
- - \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx
26687
+ ${COMMON_TOOLS}
26688
+
26689
+ ${COMMON_WORKFLOW}
26690
+ This is a third path for progress tracking alongside Contributions and Meetings.
25624
26691
 
25625
26692
  **As Delivery Manager, use Jira integration to:**
26693
+ - Use \`fetch_jira_daily\` for daily progress reports \u2014 track what moved, identify blockers, spot untracked work
25626
26694
  - Pull sprint issues for tracking progress and blockers
25627
- - Push actions, decisions, and tasks to Jira for stakeholder visibility
25628
- - Use JQL queries for reporting (e.g. \`sprint in openSprints() AND assignee = currentUser()\`)
25629
- - Sync status between Marvin governance items and Jira issues`
26695
+ - Push actions and tasks to Jira for stakeholder visibility
26696
+ - Use \`fetch_jira_daily\` with a date range for sprint retrospectives (e.g. \`from: "2026-03-10", to: "2026-03-21"\`)
26697
+ - Use \`fetch_jira_statuses\` to ensure Jira workflow statuses are properly mapped`
25630
26698
  }
25631
26699
  };
25632
26700
 
@@ -30490,12 +31558,355 @@ Run "marvin doctor --fix" to auto-repair fixable issues.`));
30490
31558
  console.log();
30491
31559
  }
30492
31560
 
31561
+ // src/cli/commands/jira.ts
31562
+ import chalk20 from "chalk";
31563
+ async function jiraSyncCommand(artifactId, options = {}) {
31564
+ const project = loadProject();
31565
+ const plugin = resolvePlugin(project.config.methodology);
31566
+ const registrations = plugin?.documentTypeRegistrations ?? [];
31567
+ const jiReg = { type: "jira-issue", dirName: "jira-issues", idPrefix: "JI" };
31568
+ const store = new DocumentStore(project.marvinDir, [...registrations, jiReg]);
31569
+ const jiraUserConfig = loadUserConfig().jira;
31570
+ const jira = createJiraClient(jiraUserConfig);
31571
+ if (!jira) {
31572
+ console.log(
31573
+ chalk20.red(
31574
+ 'Jira is not configured. Run "marvin config jira" or set JIRA_HOST, JIRA_EMAIL, and JIRA_API_TOKEN environment variables.'
31575
+ )
31576
+ );
31577
+ return;
31578
+ }
31579
+ const statusMap = project.config.jira?.statusMap;
31580
+ const label = artifactId ? `Checking ${artifactId} against Jira...` : "Checking all Jira-linked actions/tasks...";
31581
+ console.log(chalk20.dim(label));
31582
+ if (options.dryRun) {
31583
+ const fetchResult = await fetchJiraStatus(
31584
+ store,
31585
+ jira.client,
31586
+ jira.host,
31587
+ artifactId,
31588
+ statusMap
31589
+ );
31590
+ const withChanges = fetchResult.artifacts.filter(
31591
+ (a) => a.statusChanged || a.progressChanged
31592
+ );
31593
+ const noChanges = fetchResult.artifacts.filter(
31594
+ (a) => !a.statusChanged && !a.progressChanged
31595
+ );
31596
+ if (withChanges.length > 0) {
31597
+ console.log(chalk20.yellow(`
31598
+ Proposed changes for ${withChanges.length} artifact(s):`));
31599
+ for (const a of withChanges) {
31600
+ console.log(` ${chalk20.bold(a.id)} (${a.jiraKey}) \u2014 Jira: "${a.jiraSummary}"`);
31601
+ if (a.statusChanged) {
31602
+ console.log(
31603
+ ` status: ${chalk20.yellow(a.currentMarvinStatus)} \u2192 ${chalk20.green(a.proposedMarvinStatus)}`
31604
+ );
31605
+ }
31606
+ if (a.progressChanged) {
31607
+ console.log(
31608
+ ` progress: ${chalk20.yellow(String(a.currentProgress ?? 0) + "%")} \u2192 ${chalk20.green(String(a.proposedProgress) + "%")}`
31609
+ );
31610
+ }
31611
+ if (a.linkedIssues.length > 0) {
31612
+ const done = a.linkedIssues.filter((l) => l.isDone).length;
31613
+ console.log(chalk20.dim(` ${done}/${a.linkedIssues.length} linked issues done`));
31614
+ }
31615
+ }
31616
+ console.log(chalk20.dim("\nRun without --dry-run to apply these changes."));
31617
+ }
31618
+ if (noChanges.length > 0) {
31619
+ console.log(chalk20.dim(`
31620
+ ${noChanges.length} artifact(s) already in sync.`));
31621
+ }
31622
+ if (fetchResult.errors.length > 0) {
31623
+ console.log(chalk20.red("\nErrors:"));
31624
+ for (const err of fetchResult.errors) {
31625
+ console.log(chalk20.red(` ${err}`));
31626
+ }
31627
+ }
31628
+ if (fetchResult.artifacts.length === 0 && fetchResult.errors.length === 0) {
31629
+ console.log(chalk20.dim("\nNo Jira-linked actions/tasks found to check."));
31630
+ }
31631
+ return;
31632
+ }
31633
+ const result = await syncJiraProgress(
31634
+ store,
31635
+ jira.client,
31636
+ jira.host,
31637
+ artifactId,
31638
+ statusMap
31639
+ );
31640
+ if (result.updated.length > 0) {
31641
+ console.log(chalk20.green(`
31642
+ Updated ${result.updated.length} artifact(s):`));
31643
+ for (const entry of result.updated) {
31644
+ const statusChange = entry.oldStatus !== entry.newStatus ? `${chalk20.yellow(entry.oldStatus)} \u2192 ${chalk20.green(entry.newStatus)}` : chalk20.dim(entry.newStatus);
31645
+ console.log(` ${chalk20.bold(entry.id)} (${entry.jiraKey}): ${statusChange}`);
31646
+ if (entry.linkedIssues.length > 0) {
31647
+ const done = entry.linkedIssues.filter((l) => l.isDone).length;
31648
+ console.log(
31649
+ chalk20.dim(` ${done}/${entry.linkedIssues.length} linked issues done`)
31650
+ );
31651
+ for (const li of entry.linkedIssues) {
31652
+ const icon = li.isDone ? chalk20.green("\u2713") : chalk20.dim("\u25CB");
31653
+ console.log(
31654
+ chalk20.dim(` ${icon} ${li.key} ${li.summary} [${li.relationship}]`)
31655
+ );
31656
+ }
31657
+ }
31658
+ }
31659
+ }
31660
+ if (result.unchanged > 0) {
31661
+ console.log(chalk20.dim(`
31662
+ ${result.unchanged} artifact(s) unchanged.`));
31663
+ }
31664
+ if (result.errors.length > 0) {
31665
+ console.log(chalk20.red("\nErrors:"));
31666
+ for (const err of result.errors) {
31667
+ console.log(chalk20.red(` ${err}`));
31668
+ }
31669
+ }
31670
+ if (result.updated.length === 0 && result.unchanged === 0 && result.errors.length === 0) {
31671
+ console.log(chalk20.dim("\nNo Jira-linked actions/tasks found to sync."));
31672
+ }
31673
+ }
31674
+ async function jiraStatusesCommand(projectKey) {
31675
+ const project = loadProject();
31676
+ const jiraUserConfig = loadUserConfig().jira;
31677
+ const jira = createJiraClient(jiraUserConfig);
31678
+ if (!jira) {
31679
+ console.log(
31680
+ chalk20.red(
31681
+ 'Jira is not configured. Run "marvin config jira" or set JIRA_HOST, JIRA_EMAIL, and JIRA_API_TOKEN environment variables.'
31682
+ )
31683
+ );
31684
+ return;
31685
+ }
31686
+ const resolvedProjectKey = projectKey ?? project.config.jira?.projectKey;
31687
+ if (!resolvedProjectKey) {
31688
+ console.log(
31689
+ chalk20.red(
31690
+ "No project key provided. Pass it as an argument or set jira.projectKey in .marvin/config.yaml."
31691
+ )
31692
+ );
31693
+ return;
31694
+ }
31695
+ console.log(chalk20.dim(`Fetching statuses from Jira project ${resolvedProjectKey}...`));
31696
+ const statusMap = project.config.jira?.statusMap;
31697
+ const actionMap = statusMap?.action ?? DEFAULT_ACTION_STATUS_MAP;
31698
+ const taskMap = statusMap?.task ?? DEFAULT_TASK_STATUS_MAP;
31699
+ const email3 = jiraUserConfig?.email ?? process.env.JIRA_EMAIL;
31700
+ const apiToken = jiraUserConfig?.apiToken ?? process.env.JIRA_API_TOKEN;
31701
+ const auth = "Basic " + Buffer.from(`${email3}:${apiToken}`).toString("base64");
31702
+ const params = new URLSearchParams({
31703
+ jql: `project = ${resolvedProjectKey}`,
31704
+ maxResults: "100",
31705
+ fields: "status"
31706
+ });
31707
+ const resp = await fetch(`https://${jira.host}/rest/api/3/search/jql?${params}`, {
31708
+ headers: { Authorization: auth, Accept: "application/json" }
31709
+ });
31710
+ if (!resp.ok) {
31711
+ const text = await resp.text().catch(() => "");
31712
+ console.log(chalk20.red(`Jira API error ${resp.status}: ${text}`));
31713
+ return;
31714
+ }
31715
+ const data = await resp.json();
31716
+ const statusCounts = /* @__PURE__ */ new Map();
31717
+ for (const issue2 of data.issues) {
31718
+ const s = issue2.fields.status.name;
31719
+ statusCounts.set(s, (statusCounts.get(s) ?? 0) + 1);
31720
+ }
31721
+ const actionLookup = /* @__PURE__ */ new Map();
31722
+ for (const [marvin, jiraStatuses] of Object.entries(actionMap)) {
31723
+ for (const js of jiraStatuses) actionLookup.set(js.toLowerCase(), marvin);
31724
+ }
31725
+ const taskLookup = /* @__PURE__ */ new Map();
31726
+ for (const [marvin, jiraStatuses] of Object.entries(taskMap)) {
31727
+ for (const js of jiraStatuses) taskLookup.set(js.toLowerCase(), marvin);
31728
+ }
31729
+ console.log(
31730
+ `
31731
+ Found ${chalk20.bold(String(statusCounts.size))} distinct statuses in ${chalk20.bold(resolvedProjectKey)} (scanned ${data.issues.length} of ${data.total} issues):
31732
+ `
31733
+ );
31734
+ const sorted = [...statusCounts.entries()].sort((a, b) => b[1] - a[1]);
31735
+ let hasUnmapped = false;
31736
+ for (const [status, count] of sorted) {
31737
+ const actionTarget = actionLookup.get(status.toLowerCase());
31738
+ const taskTarget = taskLookup.get(status.toLowerCase());
31739
+ const actionLabel = actionTarget ? chalk20.green(`\u2192 ${actionTarget}`) : chalk20.yellow("UNMAPPED (\u2192 open)");
31740
+ const taskLabel = taskTarget ? chalk20.green(`\u2192 ${taskTarget}`) : chalk20.yellow("UNMAPPED (\u2192 backlog)");
31741
+ if (!actionTarget || !taskTarget) hasUnmapped = true;
31742
+ console.log(` ${chalk20.bold(status)} ${chalk20.dim(`(${count} issues)`)}`);
31743
+ console.log(` action: ${actionLabel}`);
31744
+ console.log(` task: ${taskLabel}`);
31745
+ }
31746
+ if (hasUnmapped) {
31747
+ console.log(chalk20.yellow("\nSome statuses are unmapped. Add jira.statusMap to .marvin/config.yaml:"));
31748
+ console.log(chalk20.dim(" jira:"));
31749
+ console.log(chalk20.dim(" statusMap:"));
31750
+ console.log(chalk20.dim(" action:"));
31751
+ console.log(chalk20.dim(' <marvin-status>: ["<Jira Status>", ...]'));
31752
+ console.log(chalk20.dim(" task:"));
31753
+ console.log(chalk20.dim(' <marvin-status>: ["<Jira Status>", ...]'));
31754
+ } else {
31755
+ console.log(chalk20.green("\nAll statuses are mapped."));
31756
+ }
31757
+ const usingConfig = statusMap?.action || statusMap?.task;
31758
+ console.log(
31759
+ chalk20.dim(
31760
+ usingConfig ? "\nUsing status maps from .marvin/config.yaml." : "\nUsing built-in default status maps (no jira.statusMap in config)."
31761
+ )
31762
+ );
31763
+ }
31764
+ async function jiraDailyCommand(options) {
31765
+ const proj = loadProject();
31766
+ const plugin = resolvePlugin(proj.config.methodology);
31767
+ const registrations = plugin?.documentTypeRegistrations ?? [];
31768
+ const jiReg = { type: "jira-issue", dirName: "jira-issues", idPrefix: "JI" };
31769
+ const store = new DocumentStore(proj.marvinDir, [...registrations, jiReg]);
31770
+ const jiraUserConfig = loadUserConfig().jira;
31771
+ const jira = createJiraClient(jiraUserConfig);
31772
+ if (!jira) {
31773
+ console.log(
31774
+ chalk20.red(
31775
+ 'Jira is not configured. Run "marvin config jira" or set JIRA_HOST, JIRA_EMAIL, and JIRA_API_TOKEN environment variables.'
31776
+ )
31777
+ );
31778
+ return;
31779
+ }
31780
+ const resolvedProjectKey = options.project ?? proj.config.jira?.projectKey;
31781
+ if (!resolvedProjectKey) {
31782
+ console.log(
31783
+ chalk20.red(
31784
+ "No project key provided. Use --project or set jira.projectKey in .marvin/config.yaml."
31785
+ )
31786
+ );
31787
+ return;
31788
+ }
31789
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
31790
+ const fromDate = options.from ?? today;
31791
+ const toDate = options.to ?? fromDate;
31792
+ const statusMap = proj.config.jira?.statusMap;
31793
+ const rangeLabel = fromDate === toDate ? fromDate : `${fromDate} to ${toDate}`;
31794
+ console.log(
31795
+ chalk20.dim(`Fetching Jira daily summary for ${resolvedProjectKey} \u2014 ${rangeLabel}...`)
31796
+ );
31797
+ const daily = await fetchJiraDaily(
31798
+ store,
31799
+ jira.client,
31800
+ jira.host,
31801
+ resolvedProjectKey,
31802
+ { from: fromDate, to: toDate },
31803
+ statusMap
31804
+ );
31805
+ console.log(
31806
+ `
31807
+ ${chalk20.bold(`Jira Daily \u2014 ${resolvedProjectKey} \u2014 ${rangeLabel}`)}`
31808
+ );
31809
+ console.log(`${daily.issues.length} issue(s) updated.
31810
+ `);
31811
+ const linked = daily.issues.filter((i) => i.marvinArtifacts.length > 0);
31812
+ const unlinked = daily.issues.filter((i) => i.marvinArtifacts.length === 0);
31813
+ if (linked.length > 0) {
31814
+ console.log(chalk20.underline("Linked Issues (with Marvin artifacts):\n"));
31815
+ for (const issue2 of linked) {
31816
+ printIssueEntry(issue2);
31817
+ }
31818
+ }
31819
+ if (unlinked.length > 0) {
31820
+ console.log(chalk20.underline("Unlinked Issues (no Marvin artifact):\n"));
31821
+ for (const issue2 of unlinked) {
31822
+ printIssueEntry(issue2);
31823
+ }
31824
+ }
31825
+ if (daily.proposedActions.length > 0) {
31826
+ console.log(chalk20.underline("Proposed Actions:\n"));
31827
+ for (const action of daily.proposedActions) {
31828
+ 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}");
31829
+ console.log(` ${icon} ${action.description}`);
31830
+ }
31831
+ console.log();
31832
+ }
31833
+ if (daily.errors.length > 0) {
31834
+ console.log(chalk20.red("Errors:"));
31835
+ for (const err of daily.errors) {
31836
+ console.log(chalk20.red(` ${err}`));
31837
+ }
31838
+ }
31839
+ if (daily.issues.length === 0 && daily.errors.length === 0) {
31840
+ console.log(chalk20.dim("No Jira activity found for this period."));
31841
+ }
31842
+ }
31843
+ function printIssueEntry(issue2) {
31844
+ const artifacts = issue2.marvinArtifacts.map((a) => a.id).join(", ");
31845
+ const artifactLabel = artifacts ? chalk20.cyan(` \u2192 ${artifacts}`) : "";
31846
+ console.log(
31847
+ ` ${chalk20.bold(issue2.key)} \u2014 ${issue2.summary} [${chalk20.yellow(issue2.currentStatus)}]${artifactLabel}`
31848
+ );
31849
+ console.log(
31850
+ chalk20.dim(` Type: ${issue2.issueType} | Assignee: ${issue2.assignee ?? "unassigned"}`)
31851
+ );
31852
+ for (const a of issue2.marvinArtifacts) {
31853
+ if (a.statusDrift) {
31854
+ console.log(
31855
+ chalk20.yellow(` \u26A0 ${a.id} status drift: Marvin="${a.currentStatus}" vs proposed="${a.proposedStatus}"`)
31856
+ );
31857
+ }
31858
+ }
31859
+ if (issue2.changes.length > 0) {
31860
+ console.log(chalk20.dim(" Changes:"));
31861
+ for (const c of issue2.changes) {
31862
+ console.log(
31863
+ chalk20.dim(` ${c.field}: ${c.from ?? "\u2205"} \u2192 ${c.to ?? "\u2205"} (${c.author}, ${c.timestamp.slice(0, 16)})`)
31864
+ );
31865
+ }
31866
+ }
31867
+ if (issue2.comments.length > 0) {
31868
+ console.log(chalk20.dim(` Comments (${issue2.comments.length}):`));
31869
+ for (const c of issue2.comments) {
31870
+ let signalLabel = "";
31871
+ if (c.signals.length > 0) {
31872
+ const labels = c.signals.map(
31873
+ (s) => s.type === "blocker" ? chalk20.red("\u{1F6AB}blocker") : s.type === "decision" ? chalk20.yellow("\u2696decision") : s.type === "question" ? chalk20.magenta("?question") : chalk20.green("\u2713resolution")
31874
+ );
31875
+ signalLabel = ` ${labels.join(" ")}`;
31876
+ }
31877
+ console.log(chalk20.dim(` ${c.author} (${c.created.slice(0, 16)})${signalLabel}: ${c.bodyPreview}`));
31878
+ }
31879
+ }
31880
+ if (issue2.linkSuggestions.length > 0) {
31881
+ console.log(chalk20.cyan(" Possible Marvin matches:"));
31882
+ for (const s of issue2.linkSuggestions) {
31883
+ console.log(
31884
+ chalk20.cyan(` \u{1F517} ${s.artifactId} ("${s.artifactTitle}") \u2014 ${Math.round(s.score * 100)}% match [${s.sharedTerms.join(", ")}]`)
31885
+ );
31886
+ }
31887
+ }
31888
+ if (issue2.linkedIssues.length > 0) {
31889
+ console.log(chalk20.dim(" Linked issues:"));
31890
+ for (const li of issue2.linkedIssues) {
31891
+ const icon = li.isDone ? chalk20.green("\u2713") : chalk20.dim("\u25CB");
31892
+ console.log(chalk20.dim(` ${icon} ${li.key} ${li.summary} [${li.relationship}] \u2014 ${li.status}`));
31893
+ }
31894
+ }
31895
+ if (issue2.confluenceLinks.length > 0) {
31896
+ console.log(chalk20.dim(" Confluence pages:"));
31897
+ for (const cl of issue2.confluenceLinks) {
31898
+ console.log(chalk20.dim(` \u{1F4C4} ${cl.title}: ${cl.url}`));
31899
+ }
31900
+ }
31901
+ console.log();
31902
+ }
31903
+
30493
31904
  // src/cli/program.ts
30494
31905
  function createProgram() {
30495
31906
  const program2 = new Command();
30496
31907
  program2.name("marvin").description(
30497
31908
  "AI-powered product development assistant with Product Owner, Delivery Manager, and Technical Lead personas"
30498
- ).version("0.5.7");
31909
+ ).version("0.5.8");
30499
31910
  program2.command("init").description("Initialize a new Marvin project in the current directory").action(async () => {
30500
31911
  await initCommand();
30501
31912
  });
@@ -30591,6 +32002,16 @@ function createProgram() {
30591
32002
  generateCmd.command("claude-md").description("Generate .marvin/CLAUDE.md project instruction file").option("--force", "Overwrite existing file without prompting").action(async (options) => {
30592
32003
  await generateClaudeMdCommand(options);
30593
32004
  });
32005
+ const jiraCmd = program2.command("jira").description("Jira integration commands");
32006
+ 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) => {
32007
+ await jiraSyncCommand(artifactId, options);
32008
+ });
32009
+ jiraCmd.command("statuses [projectKey]").description("Show Jira project statuses and their Marvin status mappings").action(async (projectKey) => {
32010
+ await jiraStatusesCommand(projectKey);
32011
+ });
32012
+ 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) => {
32013
+ await jiraDailyCommand(options);
32014
+ });
30594
32015
  return program2;
30595
32016
  }
30596
32017