mrvn-cli 0.5.17 → 0.5.18

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
@@ -15699,6 +15699,14 @@ function evaluateHealth(projectName, metrics) {
15699
15699
 
15700
15700
  // src/reports/sprint-summary/collector.ts
15701
15701
  var DONE_STATUSES3 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
15702
+ var COMPLEXITY_WEIGHTS = {
15703
+ trivial: 1,
15704
+ simple: 2,
15705
+ moderate: 3,
15706
+ complex: 5,
15707
+ "very-complex": 8
15708
+ };
15709
+ var DEFAULT_WEIGHT = 3;
15702
15710
  function collectSprintSummaryData(store, sprintId) {
15703
15711
  const allDocs = store.list();
15704
15712
  const sprintDocs = allDocs.filter((d) => d.frontmatter.type === "sprint");
@@ -15784,12 +15792,14 @@ function collectSprintSummaryData(store, sprintId) {
15784
15792
  for (const doc of workItemDocs) {
15785
15793
  const about = doc.frontmatter.aboutArtifact;
15786
15794
  const focusTag = (doc.frontmatter.tags ?? []).find((t) => t.startsWith("focus:"));
15795
+ const complexity = doc.frontmatter.complexity;
15787
15796
  const item = {
15788
15797
  id: doc.frontmatter.id,
15789
15798
  title: doc.frontmatter.title,
15790
15799
  type: doc.frontmatter.type,
15791
15800
  status: doc.frontmatter.status,
15792
15801
  progress: getEffectiveProgress(doc.frontmatter),
15802
+ weight: complexity && complexity in COMPLEXITY_WEIGHTS ? COMPLEXITY_WEIGHTS[complexity] : DEFAULT_WEIGHT,
15793
15803
  owner: doc.frontmatter.owner,
15794
15804
  workFocus: focusTag ? focusTag.slice(6) : void 0,
15795
15805
  aboutArtifact: about,
@@ -19457,7 +19467,7 @@ function poBacklogPage(ctx) {
19457
19467
  }
19458
19468
  }
19459
19469
  }
19460
- const DONE_STATUSES17 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
19470
+ const DONE_STATUSES16 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
19461
19471
  function featureTaskStats(featureId) {
19462
19472
  const fEpics = featureToEpics.get(featureId) ?? [];
19463
19473
  let total = 0;
@@ -19466,7 +19476,7 @@ function poBacklogPage(ctx) {
19466
19476
  for (const epic of fEpics) {
19467
19477
  for (const t of epicToTasks.get(epic.frontmatter.id) ?? []) {
19468
19478
  total++;
19469
- if (DONE_STATUSES17.has(t.frontmatter.status)) done++;
19479
+ if (DONE_STATUSES16.has(t.frontmatter.status)) done++;
19470
19480
  progressSum += getEffectiveProgress(t.frontmatter);
19471
19481
  }
19472
19482
  }
@@ -19712,23 +19722,34 @@ function hashString(s) {
19712
19722
  }
19713
19723
  return Math.abs(h);
19714
19724
  }
19725
+ var DONE_STATUS_SET = /* @__PURE__ */ new Set(["done", "closed", "resolved", "decided"]);
19726
+ var DEFAULT_WEIGHT2 = 3;
19715
19727
  function countFocusStats(items) {
19716
19728
  let total = 0;
19717
19729
  let done = 0;
19718
19730
  let inProgress = 0;
19719
- function walk(list) {
19731
+ let totalWeight = 0;
19732
+ let weightedSum = 0;
19733
+ function walkStats(list) {
19720
19734
  for (const w of list) {
19721
19735
  if (w.type !== "contribution") {
19722
19736
  total++;
19723
19737
  const s = w.status.toLowerCase();
19724
- if (s === "done" || s === "closed" || s === "resolved" || s === "decided") done++;
19738
+ if (DONE_STATUS_SET.has(s)) done++;
19725
19739
  else if (s === "in-progress" || s === "in progress") inProgress++;
19726
19740
  }
19727
- if (w.children) walk(w.children);
19741
+ if (w.children) walkStats(w.children);
19728
19742
  }
19729
19743
  }
19730
- walk(items);
19731
- return { total, done, inProgress };
19744
+ walkStats(items);
19745
+ for (const w of items) {
19746
+ if (w.type === "contribution") continue;
19747
+ const weight = w.weight ?? DEFAULT_WEIGHT2;
19748
+ const progress = w.progress ?? (DONE_STATUS_SET.has(w.status.toLowerCase()) ? 100 : 0);
19749
+ totalWeight += weight;
19750
+ weightedSum += weight * progress;
19751
+ }
19752
+ return { total, done, inProgress, weightedProgress: totalWeight > 0 ? Math.round(weightedSum / totalWeight) : 0 };
19732
19753
  }
19733
19754
  var KNOWN_OWNERS2 = /* @__PURE__ */ new Set(["po", "tl", "dm"]);
19734
19755
  function ownerBadge2(owner) {
@@ -19777,7 +19798,7 @@ function renderWorkItemsTable(items, options) {
19777
19798
  for (const [focus, groupItems] of focusGroups) {
19778
19799
  const color = focusColorMap.get(focus);
19779
19800
  const stats = countFocusStats(groupItems);
19780
- const pct = stats.total > 0 ? Math.round(stats.done / stats.total * 100) : 0;
19801
+ const pct = stats.weightedProgress;
19781
19802
  const summaryParts = [];
19782
19803
  if (stats.done > 0) summaryParts.push(`${stats.done} done`);
19783
19804
  if (stats.inProgress > 0) summaryParts.push(`${stats.inProgress} in progress`);
@@ -25185,6 +25206,47 @@ function extractJiraKeyFromTags(tags) {
25185
25206
  const tag = tags.find((t) => /^jira:[A-Z]+-\d+$/i.test(t));
25186
25207
  return tag ? tag.slice(5) : void 0;
25187
25208
  }
25209
+ function collectLinkedIssues(issue2) {
25210
+ const linkedIssues = [];
25211
+ if (issue2.fields.subtasks) {
25212
+ for (const sub of issue2.fields.subtasks) {
25213
+ linkedIssues.push({
25214
+ key: sub.key,
25215
+ summary: sub.fields.summary,
25216
+ status: sub.fields.status.name,
25217
+ relationship: "subtask",
25218
+ isDone: DONE_STATUSES14.has(sub.fields.status.name.toLowerCase())
25219
+ });
25220
+ }
25221
+ }
25222
+ if (issue2.fields.issuelinks) {
25223
+ for (const link of issue2.fields.issuelinks) {
25224
+ if (link.outwardIssue) {
25225
+ linkedIssues.push({
25226
+ key: link.outwardIssue.key,
25227
+ summary: link.outwardIssue.fields.summary,
25228
+ status: link.outwardIssue.fields.status.name,
25229
+ relationship: link.type.outward,
25230
+ isDone: DONE_STATUSES14.has(
25231
+ link.outwardIssue.fields.status.name.toLowerCase()
25232
+ )
25233
+ });
25234
+ }
25235
+ if (link.inwardIssue) {
25236
+ linkedIssues.push({
25237
+ key: link.inwardIssue.key,
25238
+ summary: link.inwardIssue.fields.summary,
25239
+ status: link.inwardIssue.fields.status.name,
25240
+ relationship: link.type.inward,
25241
+ isDone: DONE_STATUSES14.has(
25242
+ link.inwardIssue.fields.status.name.toLowerCase()
25243
+ )
25244
+ });
25245
+ }
25246
+ }
25247
+ }
25248
+ return linkedIssues;
25249
+ }
25188
25250
  function computeSubtaskProgress(subtasks) {
25189
25251
  if (subtasks.length === 0) return 0;
25190
25252
  const done = subtasks.filter(
@@ -25225,44 +25287,7 @@ async function fetchJiraStatus(store, client, host, artifactId, statusMap) {
25225
25287
  const resolved = statusMap ?? {};
25226
25288
  const proposedStatus = artifactType === "task" ? mapJiraStatusForTask(issue2.fields.status.name, resolved, inSprint) : mapJiraStatusForAction(issue2.fields.status.name, resolved, inSprint);
25227
25289
  const currentStatus = doc.frontmatter.status;
25228
- const linkedIssues = [];
25229
- if (issue2.fields.subtasks) {
25230
- for (const sub of issue2.fields.subtasks) {
25231
- linkedIssues.push({
25232
- key: sub.key,
25233
- summary: sub.fields.summary,
25234
- status: sub.fields.status.name,
25235
- relationship: "subtask",
25236
- isDone: DONE_STATUSES14.has(sub.fields.status.name.toLowerCase())
25237
- });
25238
- }
25239
- }
25240
- if (issue2.fields.issuelinks) {
25241
- for (const link of issue2.fields.issuelinks) {
25242
- if (link.outwardIssue) {
25243
- linkedIssues.push({
25244
- key: link.outwardIssue.key,
25245
- summary: link.outwardIssue.fields.summary,
25246
- status: link.outwardIssue.fields.status.name,
25247
- relationship: link.type.outward,
25248
- isDone: DONE_STATUSES14.has(
25249
- link.outwardIssue.fields.status.name.toLowerCase()
25250
- )
25251
- });
25252
- }
25253
- if (link.inwardIssue) {
25254
- linkedIssues.push({
25255
- key: link.inwardIssue.key,
25256
- summary: link.inwardIssue.fields.summary,
25257
- status: link.inwardIssue.fields.status.name,
25258
- relationship: link.type.inward,
25259
- isDone: DONE_STATUSES14.has(
25260
- link.inwardIssue.fields.status.name.toLowerCase()
25261
- )
25262
- });
25263
- }
25264
- }
25265
- }
25290
+ const linkedIssues = collectLinkedIssues(issue2);
25266
25291
  const subtasks = issue2.fields.subtasks ?? [];
25267
25292
  let proposedProgress;
25268
25293
  if (subtasks.length > 0 && !doc.frontmatter.progressOverride) {
@@ -25524,7 +25549,6 @@ function isWithinRange(timestamp, range) {
25524
25549
  function isConfluenceUrl(url2) {
25525
25550
  return /atlassian\.net\/wiki\//i.test(url2) || /\/confluence\//i.test(url2);
25526
25551
  }
25527
- var DONE_STATUSES15 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "obsolete", "wont do"]);
25528
25552
  async function fetchJiraDaily(store, client, host, projectKey, dateRange, statusMap) {
25529
25553
  const summary = {
25530
25554
  dateRange,
@@ -25635,42 +25659,7 @@ async function processIssue(issue2, client, host, dateRange, jiraKeyToArtifacts,
25635
25659
  });
25636
25660
  }
25637
25661
  }
25638
- const linkedIssues = [];
25639
- if (issueWithLinks) {
25640
- if (issueWithLinks.fields.subtasks) {
25641
- for (const sub of issueWithLinks.fields.subtasks) {
25642
- linkedIssues.push({
25643
- key: sub.key,
25644
- summary: sub.fields.summary,
25645
- status: sub.fields.status.name,
25646
- relationship: "subtask",
25647
- isDone: DONE_STATUSES15.has(sub.fields.status.name.toLowerCase())
25648
- });
25649
- }
25650
- }
25651
- if (issueWithLinks.fields.issuelinks) {
25652
- for (const link of issueWithLinks.fields.issuelinks) {
25653
- if (link.outwardIssue) {
25654
- linkedIssues.push({
25655
- key: link.outwardIssue.key,
25656
- summary: link.outwardIssue.fields.summary,
25657
- status: link.outwardIssue.fields.status.name,
25658
- relationship: link.type.outward,
25659
- isDone: DONE_STATUSES15.has(link.outwardIssue.fields.status.name.toLowerCase())
25660
- });
25661
- }
25662
- if (link.inwardIssue) {
25663
- linkedIssues.push({
25664
- key: link.inwardIssue.key,
25665
- summary: link.inwardIssue.fields.summary,
25666
- status: link.inwardIssue.fields.status.name,
25667
- relationship: link.type.inward,
25668
- isDone: DONE_STATUSES15.has(link.inwardIssue.fields.status.name.toLowerCase())
25669
- });
25670
- }
25671
- }
25672
- }
25673
- }
25662
+ const linkedIssues = issueWithLinks ? collectLinkedIssues(issueWithLinks) : [];
25674
25663
  const marvinArtifacts = [];
25675
25664
  const artifacts = jiraKeyToArtifacts.get(issue2.key) ?? [];
25676
25665
  for (const doc of artifacts) {
@@ -25800,22 +25789,23 @@ function generateProposedActions(issues) {
25800
25789
 
25801
25790
  // src/skills/builtin/jira/sprint-progress.ts
25802
25791
  import { query as query3 } from "@anthropic-ai/claude-agent-sdk";
25803
- var DONE_STATUSES16 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "obsolete", "wont do", "cancelled"]);
25792
+ var DONE_STATUSES15 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "obsolete", "wont do", "cancelled"]);
25804
25793
  var BATCH_SIZE = 5;
25794
+ var MAX_LINKED_ISSUES = 50;
25805
25795
  var BLOCKED_WEIGHT_RISK_THRESHOLD = 0.3;
25806
- var COMPLEXITY_WEIGHTS = {
25796
+ var COMPLEXITY_WEIGHTS2 = {
25807
25797
  trivial: 1,
25808
25798
  simple: 2,
25809
25799
  moderate: 3,
25810
25800
  complex: 5,
25811
25801
  "very-complex": 8
25812
25802
  };
25813
- var DEFAULT_WEIGHT = 3;
25803
+ var DEFAULT_WEIGHT3 = 3;
25814
25804
  function resolveWeight(complexity) {
25815
- if (complexity && complexity in COMPLEXITY_WEIGHTS) {
25816
- return { weight: COMPLEXITY_WEIGHTS[complexity], weightSource: "complexity" };
25805
+ if (complexity && complexity in COMPLEXITY_WEIGHTS2) {
25806
+ return { weight: COMPLEXITY_WEIGHTS2[complexity], weightSource: "complexity" };
25817
25807
  }
25818
- return { weight: DEFAULT_WEIGHT, weightSource: "default" };
25808
+ return { weight: DEFAULT_WEIGHT3, weightSource: "default" };
25819
25809
  }
25820
25810
  function resolveProgress(frontmatter, commentAnalysisProgress) {
25821
25811
  const hasExplicitProgress = "progress" in frontmatter && typeof frontmatter.progress === "number";
@@ -25904,6 +25894,52 @@ async function assessSprintProgress(store, client, host, options = {}) {
25904
25894
  }
25905
25895
  }
25906
25896
  }
25897
+ const linkedJiraIssues = /* @__PURE__ */ new Map();
25898
+ if (options.traverseLinks) {
25899
+ const visited = new Set(jiraIssues.keys());
25900
+ const queue = [];
25901
+ for (const [, data] of jiraIssues) {
25902
+ const links = collectLinkedIssues(data.issue);
25903
+ for (const link of links) {
25904
+ if (link.relationship !== "subtask" && !visited.has(link.key)) {
25905
+ visited.add(link.key);
25906
+ queue.push(link.key);
25907
+ }
25908
+ }
25909
+ }
25910
+ while (queue.length > 0 && linkedJiraIssues.size < MAX_LINKED_ISSUES) {
25911
+ const remaining = MAX_LINKED_ISSUES - linkedJiraIssues.size;
25912
+ const batch = queue.splice(0, Math.min(BATCH_SIZE, remaining));
25913
+ const results = await Promise.allSettled(
25914
+ batch.map(async (key) => {
25915
+ const [issue2, comments] = await Promise.all([
25916
+ client.getIssueWithLinks(key),
25917
+ client.getComments(key)
25918
+ ]);
25919
+ return { key, issue: issue2, comments };
25920
+ })
25921
+ );
25922
+ for (const result of results) {
25923
+ if (result.status === "fulfilled") {
25924
+ const { key, issue: issue2, comments } = result.value;
25925
+ linkedJiraIssues.set(key, { issue: issue2, comments });
25926
+ const newLinks = collectLinkedIssues(issue2);
25927
+ for (const link of newLinks) {
25928
+ if (link.relationship !== "subtask" && !visited.has(link.key)) {
25929
+ visited.add(link.key);
25930
+ queue.push(link.key);
25931
+ }
25932
+ }
25933
+ } else {
25934
+ const batchKey = batch[results.indexOf(result)];
25935
+ errors.push(`Failed to fetch linked issue ${batchKey}: ${result.reason instanceof Error ? result.reason.message : String(result.reason)}`);
25936
+ }
25937
+ }
25938
+ }
25939
+ if (queue.length > 0) {
25940
+ errors.push(`Link traversal capped at ${MAX_LINKED_ISSUES} linked issues (${queue.length} remaining undiscovered)`);
25941
+ }
25942
+ }
25907
25943
  const proposedUpdates = [];
25908
25944
  const itemReports = [];
25909
25945
  const childReportsByParent = /* @__PURE__ */ new Map();
@@ -25969,6 +26005,23 @@ async function assessSprintProgress(store, client, host, options = {}) {
25969
26005
  const focusTag = tags.find((t) => t.startsWith("focus:"));
25970
26006
  const { weight, weightSource } = resolveWeight(fm.complexity);
25971
26007
  const { progress: resolvedProgress, progressSource } = resolveProgress(fm, null);
26008
+ let itemLinkedIssues = [];
26009
+ const itemLinkedIssueSignals = [];
26010
+ if (options.traverseLinks && jiraData) {
26011
+ const { allLinks, allSignals } = collectTransitiveLinks(
26012
+ jiraData.issue,
26013
+ jiraIssues,
26014
+ linkedJiraIssues
26015
+ );
26016
+ itemLinkedIssues = allLinks;
26017
+ itemLinkedIssueSignals.push(...allSignals);
26018
+ analyzeLinkedIssueSignals(
26019
+ allLinks,
26020
+ fm,
26021
+ jiraKey,
26022
+ proposedUpdates
26023
+ );
26024
+ }
25972
26025
  const report = {
25973
26026
  id: fm.id,
25974
26027
  title: fm.title,
@@ -25987,6 +26040,8 @@ async function assessSprintProgress(store, client, host, options = {}) {
25987
26040
  progressDrift,
25988
26041
  commentSignals,
25989
26042
  commentSummary: null,
26043
+ linkedIssues: itemLinkedIssues,
26044
+ linkedIssueSignals: itemLinkedIssueSignals,
25990
26045
  children: [],
25991
26046
  owner: fm.owner ?? null,
25992
26047
  focusArea: focusTag ? focusTag.slice(6) : null
@@ -26030,7 +26085,7 @@ async function assessSprintProgress(store, client, host, options = {}) {
26030
26085
  const focusAreas = [];
26031
26086
  for (const [name, items] of focusAreaMap) {
26032
26087
  const allFlatItems = items.flatMap((i) => [i, ...i.children]);
26033
- const doneCount = allFlatItems.filter((i) => DONE_STATUSES16.has(i.marvinStatus)).length;
26088
+ const doneCount = allFlatItems.filter((i) => DONE_STATUSES15.has(i.marvinStatus)).length;
26034
26089
  const blockedCount = allFlatItems.filter((i) => i.marvinStatus === "blocked").length;
26035
26090
  const progress = computeWeightedProgress(items);
26036
26091
  const totalWeight = items.reduce((s, i) => s + i.weight, 0);
@@ -26082,6 +26137,26 @@ async function assessSprintProgress(store, client, host, options = {}) {
26082
26137
  errors.push(`Comment analysis failed: ${err instanceof Error ? err.message : String(err)}`);
26083
26138
  }
26084
26139
  }
26140
+ if (options.traverseLinks) {
26141
+ try {
26142
+ const linkedSummaries = await analyzeLinkedIssueComments(
26143
+ itemReports,
26144
+ linkedJiraIssues
26145
+ );
26146
+ for (const [artifactId, signalSummaries] of linkedSummaries) {
26147
+ const report = itemReports.find((r) => r.id === artifactId);
26148
+ if (!report) continue;
26149
+ for (const [sourceKey, summary] of signalSummaries) {
26150
+ const signal = report.linkedIssueSignals.find((s) => s.sourceKey === sourceKey);
26151
+ if (signal) {
26152
+ signal.commentSummary = summary;
26153
+ }
26154
+ }
26155
+ }
26156
+ } catch (err) {
26157
+ errors.push(`Linked issue comment analysis failed: ${err instanceof Error ? err.message : String(err)}`);
26158
+ }
26159
+ }
26085
26160
  }
26086
26161
  const appliedUpdates = [];
26087
26162
  if (options.applyUpdates && proposedUpdates.length > 0) {
@@ -26198,6 +26273,155 @@ ${commentTexts}`);
26198
26273
  }
26199
26274
  return summaries;
26200
26275
  }
26276
+ function collectTransitiveLinks(primaryIssue, primaryIssues, linkedJiraIssues) {
26277
+ const allLinks = [];
26278
+ const allSignals = [];
26279
+ const visited = /* @__PURE__ */ new Set([primaryIssue.key]);
26280
+ const directLinks = collectLinkedIssues(primaryIssue).filter((l) => l.relationship !== "subtask");
26281
+ const queue = [...directLinks];
26282
+ for (const link of directLinks) {
26283
+ visited.add(link.key);
26284
+ }
26285
+ while (queue.length > 0) {
26286
+ const link = queue.shift();
26287
+ allLinks.push(link);
26288
+ const linkedData = linkedJiraIssues.get(link.key) ?? primaryIssues.get(link.key);
26289
+ if (!linkedData) continue;
26290
+ const linkedCommentSignals = [];
26291
+ for (const comment of linkedData.comments) {
26292
+ const text = extractCommentText(comment.body);
26293
+ const signals = detectCommentSignals(text);
26294
+ linkedCommentSignals.push(...signals);
26295
+ }
26296
+ if (linkedCommentSignals.length > 0 || linkedData.comments.length > 0) {
26297
+ allSignals.push({
26298
+ sourceKey: link.key,
26299
+ linkType: link.relationship,
26300
+ commentSignals: linkedCommentSignals,
26301
+ commentSummary: null
26302
+ });
26303
+ }
26304
+ const nextLinks = collectLinkedIssues(linkedData.issue).filter((l) => l.relationship !== "subtask" && !visited.has(l.key));
26305
+ for (const next of nextLinks) {
26306
+ visited.add(next.key);
26307
+ queue.push(next);
26308
+ }
26309
+ }
26310
+ return { allLinks, allSignals };
26311
+ }
26312
+ var BLOCKER_LINK_PATTERNS = ["blocks", "is blocked by"];
26313
+ var WONT_DO_STATUSES = /* @__PURE__ */ new Set(["wont do", "won't do", "cancelled"]);
26314
+ function analyzeLinkedIssueSignals(linkedIssues, frontmatter, jiraKey, proposedUpdates) {
26315
+ if (linkedIssues.length === 0) return;
26316
+ const blockerLinks = linkedIssues.filter(
26317
+ (l) => BLOCKER_LINK_PATTERNS.some((p) => l.relationship.toLowerCase().includes(p.split(" ")[0]))
26318
+ );
26319
+ if (blockerLinks.length > 0 && blockerLinks.every((l) => l.isDone) && frontmatter.status === "blocked") {
26320
+ proposedUpdates.push({
26321
+ artifactId: frontmatter.id,
26322
+ field: "status",
26323
+ currentValue: "blocked",
26324
+ proposedValue: "in-progress",
26325
+ reason: `All blocking issues resolved: ${blockerLinks.map((l) => l.key).join(", ")}`
26326
+ });
26327
+ }
26328
+ const wontDoLinks = linkedIssues.filter(
26329
+ (l) => WONT_DO_STATUSES.has(l.status.toLowerCase())
26330
+ );
26331
+ if (wontDoLinks.length > 0) {
26332
+ proposedUpdates.push({
26333
+ artifactId: frontmatter.id,
26334
+ field: "review",
26335
+ currentValue: null,
26336
+ proposedValue: "needs-review",
26337
+ reason: `Linked issue(s) cancelled/won't do: ${wontDoLinks.map((l) => `${l.key} "${l.summary}"`).join(", ")}`
26338
+ });
26339
+ }
26340
+ }
26341
+ var LINKED_COMMENT_ANALYSIS_PROMPT = `You are a delivery management assistant analyzing Jira comments from linked issues for progress signals.
26342
+
26343
+ For each linked issue below, read the comments and produce a 1-sentence summary focused on: impact on the parent issue, blockers, or decisions.
26344
+
26345
+ Return your response as a JSON object mapping artifact IDs to objects mapping linked issue keys to summary strings.
26346
+ Example: {"T-001": {"PROJ-301": "DBA review scheduled for Thursday."}}
26347
+
26348
+ IMPORTANT: Only return the JSON object, no other text.`;
26349
+ async function analyzeLinkedIssueComments(items, linkedJiraIssues) {
26350
+ const results = /* @__PURE__ */ new Map();
26351
+ const promptParts = [];
26352
+ const itemsWithLinkedComments = [];
26353
+ for (const item of items) {
26354
+ if (item.linkedIssueSignals.length === 0) continue;
26355
+ const linkedParts = [];
26356
+ for (const signal of item.linkedIssueSignals) {
26357
+ const linkedData = linkedJiraIssues.get(signal.sourceKey);
26358
+ if (!linkedData || linkedData.comments.length === 0) continue;
26359
+ const commentTexts = linkedData.comments.map((c) => {
26360
+ const text = extractCommentText(c.body);
26361
+ return ` [${c.author.displayName}, ${c.created.slice(0, 10)}]: ${text.slice(0, 300)}`;
26362
+ }).join("\n");
26363
+ linkedParts.push(` ### ${signal.sourceKey} (${signal.linkType})
26364
+ ${commentTexts}`);
26365
+ }
26366
+ if (linkedParts.length > 0) {
26367
+ itemsWithLinkedComments.push(item);
26368
+ promptParts.push(`## ${item.id} \u2014 ${item.title}
26369
+ Linked issues:
26370
+ ${linkedParts.join("\n")}`);
26371
+ }
26372
+ }
26373
+ if (promptParts.length === 0) return results;
26374
+ const prompt = promptParts.join("\n\n");
26375
+ const llmResult = query3({
26376
+ prompt,
26377
+ options: {
26378
+ systemPrompt: LINKED_COMMENT_ANALYSIS_PROMPT,
26379
+ maxTurns: 1,
26380
+ tools: [],
26381
+ allowedTools: []
26382
+ }
26383
+ });
26384
+ for await (const msg of llmResult) {
26385
+ if (msg.type === "assistant") {
26386
+ const textBlock = msg.message.content.find(
26387
+ (b) => b.type === "text"
26388
+ );
26389
+ if (textBlock) {
26390
+ const parsed = parseLlmJson(textBlock.text);
26391
+ if (parsed) {
26392
+ for (const [artifactId, linkedSummaries] of Object.entries(parsed)) {
26393
+ if (typeof linkedSummaries === "object" && linkedSummaries !== null) {
26394
+ const signalMap = /* @__PURE__ */ new Map();
26395
+ for (const [key, summary] of Object.entries(linkedSummaries)) {
26396
+ if (typeof summary === "string") {
26397
+ signalMap.set(key, summary);
26398
+ }
26399
+ }
26400
+ if (signalMap.size > 0) {
26401
+ results.set(artifactId, signalMap);
26402
+ }
26403
+ }
26404
+ }
26405
+ }
26406
+ }
26407
+ }
26408
+ }
26409
+ return results;
26410
+ }
26411
+ function parseLlmJson(text) {
26412
+ try {
26413
+ return JSON.parse(text);
26414
+ } catch {
26415
+ const match = text.match(/```(?:json)?\s*([\s\S]*?)```/);
26416
+ if (match) {
26417
+ try {
26418
+ return JSON.parse(match[1]);
26419
+ } catch {
26420
+ }
26421
+ }
26422
+ return null;
26423
+ }
26424
+ }
26201
26425
  function formatProgressReport(report) {
26202
26426
  const parts = [];
26203
26427
  parts.push(`# Sprint Progress Assessment \u2014 ${report.sprintId}`);
@@ -26281,7 +26505,7 @@ function formatProgressReport(report) {
26281
26505
  }
26282
26506
  function formatItemLine(parts, item, depth) {
26283
26507
  const indent = " ".repeat(depth + 1);
26284
- const statusIcon = DONE_STATUSES16.has(item.marvinStatus) ? "\u2713" : item.marvinStatus === "blocked" ? "\u{1F6AB}" : item.marvinStatus === "in-progress" ? "\u25B6" : "\u25CB";
26508
+ const statusIcon = DONE_STATUSES15.has(item.marvinStatus) ? "\u2713" : item.marvinStatus === "blocked" ? "\u{1F6AB}" : item.marvinStatus === "in-progress" ? "\u25B6" : "\u25CB";
26285
26509
  const jiraLabel = item.jiraKey ? ` [${item.jiraKey}: ${item.jiraStatus}]` : "";
26286
26510
  const driftFlag = item.statusDrift ? " \u26A0drift" : "";
26287
26511
  const progressLabel = ` ${item.progress}%`;
@@ -26291,6 +26515,19 @@ function formatItemLine(parts, item, depth) {
26291
26515
  if (item.commentSummary) {
26292
26516
  parts.push(`${indent} \u{1F4AC} ${item.commentSummary}`);
26293
26517
  }
26518
+ if (item.linkedIssues.length > 0) {
26519
+ parts.push(`${indent} \u{1F517} Linked Issues:`);
26520
+ for (const link of item.linkedIssues) {
26521
+ const doneMarker = link.isDone ? " \u2713" : "";
26522
+ const blockerResolved = link.isDone && BLOCKER_LINK_PATTERNS.some((p) => link.relationship.toLowerCase().includes(p.split(" ")[0])) ? " unblock signal" : "";
26523
+ const wontDo = WONT_DO_STATUSES.has(link.status.toLowerCase()) ? " \u26A0 needs review" : "";
26524
+ parts.push(`${indent} ${link.relationship} ${link.key} "${link.summary}" [${link.status}]${doneMarker}${blockerResolved}${wontDo}`);
26525
+ const signal = item.linkedIssueSignals.find((s) => s.sourceKey === link.key);
26526
+ if (signal?.commentSummary) {
26527
+ parts.push(`${indent} \u{1F4AC} ${signal.commentSummary}`);
26528
+ }
26529
+ }
26530
+ }
26294
26531
  for (const child of item.children) {
26295
26532
  formatItemLine(parts, child, depth + 1);
26296
26533
  }
@@ -26300,6 +26537,506 @@ function progressBar6(pct) {
26300
26537
  const empty = 10 - filled;
26301
26538
  return `[${"\u2588".repeat(filled)}${"\u2591".repeat(empty)}]`;
26302
26539
  }
26540
+ var MAX_ARTIFACT_NODES = 50;
26541
+ var MAX_LLM_DEPTH = 3;
26542
+ var MAX_LLM_COMMENT_CHARS = 8e3;
26543
+ async function assessArtifact(store, client, host, options) {
26544
+ const visited = /* @__PURE__ */ new Set();
26545
+ return _assessArtifactRecursive(store, client, host, options, visited, 0);
26546
+ }
26547
+ async function _assessArtifactRecursive(store, client, host, options, visited, depth) {
26548
+ const errors = [];
26549
+ if (visited.has(options.artifactId)) {
26550
+ return emptyArtifactReport(options.artifactId, [`Cycle detected: ${options.artifactId} already visited`]);
26551
+ }
26552
+ if (visited.size >= MAX_ARTIFACT_NODES) {
26553
+ return emptyArtifactReport(options.artifactId, [`Node cap reached (${MAX_ARTIFACT_NODES}), skipping ${options.artifactId}`]);
26554
+ }
26555
+ visited.add(options.artifactId);
26556
+ const doc = store.get(options.artifactId);
26557
+ if (!doc) {
26558
+ return emptyArtifactReport(options.artifactId, [`Artifact ${options.artifactId} not found`]);
26559
+ }
26560
+ const fm = doc.frontmatter;
26561
+ const jiraKey = fm.jiraKey ?? extractJiraKeyFromTags(fm.tags) ?? null;
26562
+ const tags = fm.tags ?? [];
26563
+ const sprintTag = tags.find((t) => t.startsWith("sprint:"));
26564
+ const sprint = sprintTag ? sprintTag.slice(7) : null;
26565
+ const parent = fm.aboutArtifact ?? null;
26566
+ let jiraStatus = null;
26567
+ let jiraAssignee = null;
26568
+ let proposedMarvinStatus = null;
26569
+ let jiraSubtaskProgress = null;
26570
+ const commentSignals = [];
26571
+ let commentSummary = null;
26572
+ let linkedIssues = [];
26573
+ let linkedIssueSignals = [];
26574
+ const proposedUpdates = [];
26575
+ const jiraIssues = /* @__PURE__ */ new Map();
26576
+ const linkedJiraIssues = /* @__PURE__ */ new Map();
26577
+ if (jiraKey) {
26578
+ try {
26579
+ const [issue2, comments] = await Promise.all([
26580
+ client.getIssueWithLinks(jiraKey),
26581
+ client.getComments(jiraKey)
26582
+ ]);
26583
+ jiraIssues.set(jiraKey, { issue: issue2, comments });
26584
+ jiraStatus = issue2.fields.status.name;
26585
+ jiraAssignee = issue2.fields.assignee?.displayName ?? null;
26586
+ const inSprint = isInActiveSprint(store, fm.tags);
26587
+ const resolved = options.statusMap ?? {};
26588
+ proposedMarvinStatus = fm.type === "task" ? mapJiraStatusForTask(jiraStatus, resolved, inSprint) : mapJiraStatusForAction(jiraStatus, resolved, inSprint);
26589
+ const subtasks = issue2.fields.subtasks ?? [];
26590
+ if (subtasks.length > 0) {
26591
+ jiraSubtaskProgress = computeSubtaskProgress(subtasks);
26592
+ }
26593
+ for (const comment of comments) {
26594
+ const text = extractCommentText(comment.body);
26595
+ const signals2 = detectCommentSignals(text);
26596
+ commentSignals.push(...signals2);
26597
+ }
26598
+ const jiraVisited = /* @__PURE__ */ new Set([jiraKey]);
26599
+ const queue = [];
26600
+ const directLinks = collectLinkedIssues(issue2).filter((l) => l.relationship !== "subtask");
26601
+ for (const link of directLinks) {
26602
+ if (!jiraVisited.has(link.key)) {
26603
+ jiraVisited.add(link.key);
26604
+ queue.push(link.key);
26605
+ }
26606
+ }
26607
+ while (queue.length > 0 && linkedJiraIssues.size < MAX_LINKED_ISSUES) {
26608
+ const remaining = MAX_LINKED_ISSUES - linkedJiraIssues.size;
26609
+ const batch = queue.splice(0, Math.min(BATCH_SIZE, remaining));
26610
+ const results = await Promise.allSettled(
26611
+ batch.map(async (key) => {
26612
+ const [li, lc] = await Promise.all([
26613
+ client.getIssueWithLinks(key),
26614
+ client.getComments(key)
26615
+ ]);
26616
+ return { key, issue: li, comments: lc };
26617
+ })
26618
+ );
26619
+ for (const result of results) {
26620
+ if (result.status === "fulfilled") {
26621
+ const { key, issue: li, comments: lc } = result.value;
26622
+ linkedJiraIssues.set(key, { issue: li, comments: lc });
26623
+ const newLinks = collectLinkedIssues(li).filter((l) => l.relationship !== "subtask" && !jiraVisited.has(l.key));
26624
+ for (const nl of newLinks) {
26625
+ jiraVisited.add(nl.key);
26626
+ queue.push(nl.key);
26627
+ }
26628
+ } else {
26629
+ const batchKey = batch[results.indexOf(result)];
26630
+ errors.push(`Failed to fetch linked issue ${batchKey}: ${result.reason instanceof Error ? result.reason.message : String(result.reason)}`);
26631
+ }
26632
+ }
26633
+ }
26634
+ const { allLinks, allSignals } = collectTransitiveLinks(
26635
+ issue2,
26636
+ jiraIssues,
26637
+ linkedJiraIssues
26638
+ );
26639
+ linkedIssues = allLinks;
26640
+ linkedIssueSignals = allSignals;
26641
+ analyzeLinkedIssueSignals(allLinks, fm, jiraKey, proposedUpdates);
26642
+ } catch (err) {
26643
+ errors.push(`Failed to fetch ${jiraKey}: ${err instanceof Error ? err.message : String(err)}`);
26644
+ }
26645
+ }
26646
+ const currentProgress = getEffectiveProgress(fm);
26647
+ const statusDrift = proposedMarvinStatus !== null && proposedMarvinStatus !== fm.status;
26648
+ const progressDrift = jiraSubtaskProgress !== null && !fm.progressOverride && jiraSubtaskProgress !== currentProgress;
26649
+ if (statusDrift && proposedMarvinStatus) {
26650
+ proposedUpdates.push({
26651
+ artifactId: fm.id,
26652
+ field: "status",
26653
+ currentValue: fm.status,
26654
+ proposedValue: proposedMarvinStatus,
26655
+ reason: `Jira ${jiraKey} is "${jiraStatus}" \u2192 maps to "${proposedMarvinStatus}"`
26656
+ });
26657
+ }
26658
+ if (progressDrift && jiraSubtaskProgress !== null) {
26659
+ proposedUpdates.push({
26660
+ artifactId: fm.id,
26661
+ field: "progress",
26662
+ currentValue: currentProgress,
26663
+ proposedValue: jiraSubtaskProgress,
26664
+ reason: `Jira ${jiraKey} subtask progress is ${jiraSubtaskProgress}%`
26665
+ });
26666
+ } else if (statusDrift && proposedMarvinStatus && !fm.progressOverride) {
26667
+ const hasExplicitProgress = "progress" in fm && typeof fm.progress === "number";
26668
+ if (!hasExplicitProgress) {
26669
+ const proposedProgress = STATUS_PROGRESS_DEFAULTS[proposedMarvinStatus] ?? 0;
26670
+ if (proposedProgress !== currentProgress) {
26671
+ proposedUpdates.push({
26672
+ artifactId: fm.id,
26673
+ field: "progress",
26674
+ currentValue: currentProgress,
26675
+ proposedValue: proposedProgress,
26676
+ reason: `Status changing to "${proposedMarvinStatus}" \u2192 default progress ${proposedProgress}%`
26677
+ });
26678
+ }
26679
+ }
26680
+ }
26681
+ const primaryHasComments = jiraKey ? (jiraIssues.get(jiraKey)?.comments.length ?? 0) > 0 : false;
26682
+ if (depth < MAX_LLM_DEPTH && jiraKey && primaryHasComments) {
26683
+ const estimatedChars = estimateCommentTextSize(jiraIssues, linkedJiraIssues, linkedIssueSignals);
26684
+ if (estimatedChars <= MAX_LLM_COMMENT_CHARS) {
26685
+ try {
26686
+ const summary = await analyzeSingleArtifactComments(
26687
+ fm.id,
26688
+ fm.title,
26689
+ jiraKey,
26690
+ jiraStatus,
26691
+ jiraIssues,
26692
+ linkedJiraIssues,
26693
+ linkedIssueSignals
26694
+ );
26695
+ commentSummary = summary;
26696
+ } catch (err) {
26697
+ errors.push(`Comment analysis failed: ${err instanceof Error ? err.message : String(err)}`);
26698
+ }
26699
+ }
26700
+ }
26701
+ const childIds = findChildIds(store, fm);
26702
+ const children = [];
26703
+ for (const childId of childIds) {
26704
+ if (visited.size >= MAX_ARTIFACT_NODES) {
26705
+ errors.push(`Node cap reached (${MAX_ARTIFACT_NODES}), ${childIds.length - children.length} children skipped`);
26706
+ break;
26707
+ }
26708
+ const childReport = await _assessArtifactRecursive(
26709
+ store,
26710
+ client,
26711
+ host,
26712
+ { ...options, artifactId: childId },
26713
+ visited,
26714
+ depth + 1
26715
+ );
26716
+ children.push(childReport);
26717
+ }
26718
+ const signals = buildSignals(commentSignals, linkedIssues, statusDrift, proposedMarvinStatus);
26719
+ const appliedUpdates = [];
26720
+ if (options.applyUpdates && proposedUpdates.length > 0) {
26721
+ for (const update of proposedUpdates) {
26722
+ if (update.field === "review") continue;
26723
+ try {
26724
+ store.update(update.artifactId, {
26725
+ [update.field]: update.proposedValue,
26726
+ lastJiraSyncAt: (/* @__PURE__ */ new Date()).toISOString()
26727
+ });
26728
+ const updatedDoc = store.get(update.artifactId);
26729
+ if (updatedDoc) {
26730
+ if (updatedDoc.frontmatter.type === "task") {
26731
+ propagateProgressFromTask(store, update.artifactId);
26732
+ } else if (updatedDoc.frontmatter.type === "action") {
26733
+ propagateProgressToAction(store, update.artifactId);
26734
+ }
26735
+ }
26736
+ appliedUpdates.push(update);
26737
+ } catch (err) {
26738
+ errors.push(`Failed to apply update: ${err instanceof Error ? err.message : String(err)}`);
26739
+ }
26740
+ }
26741
+ }
26742
+ return {
26743
+ artifactId: fm.id,
26744
+ title: fm.title,
26745
+ type: fm.type,
26746
+ marvinStatus: fm.status,
26747
+ marvinProgress: currentProgress,
26748
+ sprint,
26749
+ parent,
26750
+ jiraKey,
26751
+ jiraStatus,
26752
+ jiraAssignee,
26753
+ jiraSubtaskProgress,
26754
+ proposedMarvinStatus,
26755
+ statusDrift,
26756
+ progressDrift,
26757
+ commentSignals,
26758
+ commentSummary,
26759
+ linkedIssues,
26760
+ linkedIssueSignals,
26761
+ children,
26762
+ proposedUpdates: options.applyUpdates ? [] : proposedUpdates,
26763
+ appliedUpdates,
26764
+ signals,
26765
+ errors
26766
+ };
26767
+ }
26768
+ function findChildIds(store, fm) {
26769
+ if (fm.type === "action") {
26770
+ return store.list({ type: "task" }).filter((d) => d.frontmatter.aboutArtifact === fm.id).map((d) => d.frontmatter.id);
26771
+ }
26772
+ if (fm.type === "epic") {
26773
+ const epicTag = `epic:${fm.id}`;
26774
+ const isLinked = (d) => {
26775
+ const le = d.frontmatter.linkedEpic;
26776
+ if (le?.includes(fm.id)) return true;
26777
+ const t = d.frontmatter.tags ?? [];
26778
+ return t.includes(epicTag);
26779
+ };
26780
+ return [
26781
+ ...store.list({ type: "action" }).filter(isLinked),
26782
+ ...store.list({ type: "task" }).filter(isLinked)
26783
+ ].map((d) => d.frontmatter.id);
26784
+ }
26785
+ return [];
26786
+ }
26787
+ function buildSignals(commentSignals, linkedIssues, statusDrift, proposedStatus) {
26788
+ const signals = [];
26789
+ const blockerSignals = commentSignals.filter((s) => s.type === "blocker");
26790
+ if (blockerSignals.length > 0) {
26791
+ for (const s of blockerSignals) {
26792
+ signals.push(`\u{1F6AB} Blocker \u2014 "${s.snippet}"`);
26793
+ }
26794
+ }
26795
+ const blockingLinks = linkedIssues.filter(
26796
+ (l) => l.relationship.toLowerCase().includes("block")
26797
+ );
26798
+ const activeBlockers = blockingLinks.filter((l) => !l.isDone);
26799
+ const resolvedBlockers = blockingLinks.filter((l) => l.isDone);
26800
+ if (activeBlockers.length > 0) {
26801
+ for (const b of activeBlockers) {
26802
+ signals.push(`\u{1F6AB} Blocker \u2014 ${b.relationship} ${b.key} "${b.summary}" [${b.status}]`);
26803
+ }
26804
+ }
26805
+ if (resolvedBlockers.length > 0 && activeBlockers.length === 0) {
26806
+ signals.push(`\u2705 Unblocked \u2014 all blocking issues resolved: ${resolvedBlockers.map((l) => l.key).join(", ")}`);
26807
+ }
26808
+ const wontDoLinks = linkedIssues.filter((l) => WONT_DO_STATUSES.has(l.status.toLowerCase()));
26809
+ for (const l of wontDoLinks) {
26810
+ signals.push(`\u{1F504} Superseded \u2014 ${l.key} "${l.summary}" is ${l.status}`);
26811
+ }
26812
+ const questionSignals = commentSignals.filter((s) => s.type === "question");
26813
+ for (const s of questionSignals) {
26814
+ signals.push(`\u23F3 Waiting \u2014 "${s.snippet}"`);
26815
+ }
26816
+ const relatedInProgress = linkedIssues.filter(
26817
+ (l) => l.relationship.toLowerCase().includes("relate") && !l.isDone
26818
+ );
26819
+ if (relatedInProgress.length > 0) {
26820
+ for (const l of relatedInProgress) {
26821
+ signals.push(`\u{1F4CB} Handoff \u2014 related work on ${l.key} "${l.summary}" [${l.status}]`);
26822
+ }
26823
+ }
26824
+ if (signals.length === 0) {
26825
+ if (statusDrift && proposedStatus) {
26826
+ signals.push(`\u26A0 Drift detected \u2014 Marvin and Jira statuses diverge`);
26827
+ } else {
26828
+ signals.push(`\u2705 No active blockers or concerns detected`);
26829
+ }
26830
+ }
26831
+ return signals;
26832
+ }
26833
+ function estimateCommentTextSize(jiraIssues, linkedJiraIssues, linkedIssueSignals) {
26834
+ let total = 0;
26835
+ for (const [, data] of jiraIssues) {
26836
+ for (const c of data.comments) {
26837
+ total += typeof c.body === "string" ? c.body.length : JSON.stringify(c.body).length;
26838
+ }
26839
+ }
26840
+ for (const signal of linkedIssueSignals) {
26841
+ const linkedData = linkedJiraIssues.get(signal.sourceKey);
26842
+ if (!linkedData) continue;
26843
+ for (const c of linkedData.comments) {
26844
+ total += typeof c.body === "string" ? c.body.length : JSON.stringify(c.body).length;
26845
+ }
26846
+ }
26847
+ return total;
26848
+ }
26849
+ var SINGLE_ARTIFACT_COMMENT_PROMPT = `You are a delivery management assistant analyzing Jira comments for a single work item.
26850
+
26851
+ Produce a 2-3 sentence progress summary covering:
26852
+ - What work has been completed
26853
+ - What is pending or blocked
26854
+ - Any decisions, handoffs, or scheduling mentioned
26855
+ - Relevant context from linked issue comments (if provided)
26856
+
26857
+ Return ONLY the summary text, no JSON or formatting.`;
26858
+ async function analyzeSingleArtifactComments(artifactId, title, jiraKey, jiraStatus, jiraIssues, linkedJiraIssues, linkedIssueSignals) {
26859
+ const promptParts = [];
26860
+ const primaryData = jiraIssues.get(jiraKey);
26861
+ if (primaryData && primaryData.comments.length > 0) {
26862
+ const commentTexts = primaryData.comments.map((c) => {
26863
+ const text = extractCommentText(c.body);
26864
+ return `[${c.author.displayName}, ${c.created.slice(0, 10)}]: ${text.slice(0, 500)}`;
26865
+ }).join("\n");
26866
+ promptParts.push(`## ${artifactId} \u2014 ${title} (${jiraKey}, status: ${jiraStatus})
26867
+ Comments:
26868
+ ${commentTexts}`);
26869
+ }
26870
+ for (const signal of linkedIssueSignals) {
26871
+ const linkedData = linkedJiraIssues.get(signal.sourceKey);
26872
+ if (!linkedData || linkedData.comments.length === 0) continue;
26873
+ const commentTexts = linkedData.comments.map((c) => {
26874
+ const text = extractCommentText(c.body);
26875
+ return ` [${c.author.displayName}, ${c.created.slice(0, 10)}]: ${text.slice(0, 300)}`;
26876
+ }).join("\n");
26877
+ promptParts.push(`### Linked: ${signal.sourceKey} (${signal.linkType})
26878
+ ${commentTexts}`);
26879
+ }
26880
+ if (promptParts.length === 0) return null;
26881
+ const prompt = promptParts.join("\n\n");
26882
+ const result = query3({
26883
+ prompt,
26884
+ options: {
26885
+ systemPrompt: SINGLE_ARTIFACT_COMMENT_PROMPT,
26886
+ maxTurns: 1,
26887
+ tools: [],
26888
+ allowedTools: []
26889
+ }
26890
+ });
26891
+ for await (const msg of result) {
26892
+ if (msg.type === "assistant") {
26893
+ const textBlock = msg.message.content.find(
26894
+ (b) => b.type === "text"
26895
+ );
26896
+ if (textBlock) {
26897
+ return textBlock.text.trim();
26898
+ }
26899
+ }
26900
+ }
26901
+ return null;
26902
+ }
26903
+ function emptyArtifactReport(artifactId, errors) {
26904
+ return {
26905
+ artifactId,
26906
+ title: "Not found",
26907
+ type: "unknown",
26908
+ marvinStatus: "unknown",
26909
+ marvinProgress: 0,
26910
+ sprint: null,
26911
+ parent: null,
26912
+ jiraKey: null,
26913
+ jiraStatus: null,
26914
+ jiraAssignee: null,
26915
+ jiraSubtaskProgress: null,
26916
+ proposedMarvinStatus: null,
26917
+ statusDrift: false,
26918
+ progressDrift: false,
26919
+ commentSignals: [],
26920
+ commentSummary: null,
26921
+ linkedIssues: [],
26922
+ linkedIssueSignals: [],
26923
+ children: [],
26924
+ proposedUpdates: [],
26925
+ appliedUpdates: [],
26926
+ signals: [],
26927
+ errors
26928
+ };
26929
+ }
26930
+ function formatArtifactReport(report) {
26931
+ const parts = [];
26932
+ parts.push(`# Artifact Assessment \u2014 ${report.artifactId}`);
26933
+ parts.push(report.title);
26934
+ parts.push("");
26935
+ parts.push(`## Marvin State`);
26936
+ const marvinParts = [`Status: ${report.marvinStatus}`, `Progress: ${report.marvinProgress}%`];
26937
+ if (report.sprint) marvinParts.push(`Sprint: ${report.sprint}`);
26938
+ if (report.parent) marvinParts.push(`Parent: ${report.parent}`);
26939
+ parts.push(marvinParts.join(" | "));
26940
+ parts.push("");
26941
+ if (report.jiraKey) {
26942
+ parts.push(`## Jira State (${report.jiraKey})`);
26943
+ const jiraParts = [`Status: ${report.jiraStatus ?? "unknown"}`];
26944
+ if (report.jiraAssignee) jiraParts.push(`Assignee: ${report.jiraAssignee}`);
26945
+ if (report.jiraSubtaskProgress !== null) jiraParts.push(`Subtask progress: ${report.jiraSubtaskProgress}%`);
26946
+ parts.push(jiraParts.join(" | "));
26947
+ if (report.statusDrift) {
26948
+ parts.push(`\u26A0 Drift: ${report.marvinStatus} \u2192 ${report.proposedMarvinStatus}`);
26949
+ }
26950
+ if (report.progressDrift && report.jiraSubtaskProgress !== null) {
26951
+ parts.push(`\u26A0 Progress drift: ${report.marvinProgress}% \u2192 ${report.jiraSubtaskProgress}%`);
26952
+ }
26953
+ parts.push("");
26954
+ }
26955
+ if (report.commentSummary) {
26956
+ parts.push(`## Comments`);
26957
+ parts.push(report.commentSummary);
26958
+ parts.push("");
26959
+ }
26960
+ if (report.children.length > 0) {
26961
+ const doneCount = report.children.filter((c) => DONE_STATUSES15.has(c.marvinStatus)).length;
26962
+ const childWeights = report.children.map((c) => {
26963
+ const { weight } = resolveWeight(void 0);
26964
+ return { weight, progress: c.marvinProgress };
26965
+ });
26966
+ const childProgress = childWeights.length > 0 ? Math.round(childWeights.reduce((s, c) => s + c.weight * c.progress, 0) / childWeights.reduce((s, c) => s + c.weight, 0)) : 0;
26967
+ const bar = progressBar6(childProgress);
26968
+ parts.push(`## Children (${doneCount}/${report.children.length} done) ${bar} ${childProgress}%`);
26969
+ for (const child of report.children) {
26970
+ formatArtifactChild(parts, child, 1);
26971
+ }
26972
+ parts.push("");
26973
+ }
26974
+ if (report.linkedIssues.length > 0) {
26975
+ parts.push(`## Linked Issues (${report.linkedIssues.length})`);
26976
+ for (const link of report.linkedIssues) {
26977
+ const doneMarker = link.isDone ? " \u2713" : "";
26978
+ parts.push(` ${link.relationship} ${link.key} "${link.summary}" [${link.status}]${doneMarker}`);
26979
+ const signal = report.linkedIssueSignals.find((s) => s.sourceKey === link.key);
26980
+ if (signal?.commentSummary) {
26981
+ parts.push(` \u{1F4AC} ${signal.commentSummary}`);
26982
+ }
26983
+ }
26984
+ parts.push("");
26985
+ }
26986
+ if (report.signals.length > 0) {
26987
+ parts.push(`## Signals`);
26988
+ for (const s of report.signals) {
26989
+ parts.push(` ${s}`);
26990
+ }
26991
+ parts.push("");
26992
+ }
26993
+ if (report.proposedUpdates.length > 0) {
26994
+ parts.push(`## Proposed Updates (${report.proposedUpdates.length})`);
26995
+ for (const update of report.proposedUpdates) {
26996
+ parts.push(` ${update.artifactId}.${update.field}: ${String(update.currentValue)} \u2192 ${String(update.proposedValue)}`);
26997
+ parts.push(` Reason: ${update.reason}`);
26998
+ }
26999
+ parts.push("");
27000
+ parts.push("Run with applyUpdates=true to apply these changes.");
27001
+ parts.push("");
27002
+ }
27003
+ if (report.appliedUpdates.length > 0) {
27004
+ parts.push(`## Applied Updates (${report.appliedUpdates.length})`);
27005
+ for (const update of report.appliedUpdates) {
27006
+ parts.push(` \u2713 ${update.artifactId}.${update.field}: ${String(update.currentValue)} \u2192 ${String(update.proposedValue)}`);
27007
+ }
27008
+ parts.push("");
27009
+ }
27010
+ if (report.errors.length > 0) {
27011
+ parts.push(`## Errors`);
27012
+ for (const err of report.errors) {
27013
+ parts.push(` ${err}`);
27014
+ }
27015
+ parts.push("");
27016
+ }
27017
+ return parts.join("\n");
27018
+ }
27019
+ function formatArtifactChild(parts, child, depth) {
27020
+ const indent = " ".repeat(depth);
27021
+ const icon = DONE_STATUSES15.has(child.marvinStatus) ? "\u2713" : child.marvinStatus === "blocked" ? "\u{1F6AB}" : child.marvinStatus === "in-progress" ? "\u25B6" : "\u25CB";
27022
+ const jiraLabel = child.jiraKey ? ` [${child.jiraKey}: ${child.jiraStatus ?? "?"}]` : "";
27023
+ const driftLabel = child.statusDrift ? ` \u26A0drift \u2192 ${child.proposedMarvinStatus}` : "";
27024
+ const signalHints = [];
27025
+ for (const s of child.signals) {
27026
+ if (s.startsWith("\u2705 No active")) continue;
27027
+ signalHints.push(s);
27028
+ }
27029
+ parts.push(`${indent}${icon} ${child.artifactId} \u2014 ${child.title} [${child.marvinStatus}] ${child.marvinProgress}%${jiraLabel}${driftLabel}`);
27030
+ if (child.commentSummary) {
27031
+ parts.push(`${indent} \u{1F4AC} ${child.commentSummary}`);
27032
+ }
27033
+ for (const hint of signalHints) {
27034
+ parts.push(`${indent} ${hint}`);
27035
+ }
27036
+ for (const grandchild of child.children) {
27037
+ formatArtifactChild(parts, grandchild, depth + 1);
27038
+ }
27039
+ }
26303
27040
 
26304
27041
  // src/skills/builtin/jira/tools.ts
26305
27042
  var JIRA_TYPE = "jira-issue";
@@ -27086,7 +27823,8 @@ function createJiraTools(store, projectConfig) {
27086
27823
  {
27087
27824
  sprintId: external_exports.string().optional().describe("Sprint ID (e.g. 'SP-001'). Defaults to active sprint."),
27088
27825
  analyzeComments: external_exports.boolean().optional().describe("Use LLM to summarize Jira comments for progress signals (default false)"),
27089
- applyUpdates: external_exports.boolean().optional().describe("Apply proposed status/progress updates to Marvin artifacts (default false)")
27826
+ applyUpdates: external_exports.boolean().optional().describe("Apply proposed status/progress updates to Marvin artifacts (default false)"),
27827
+ traverseLinks: external_exports.boolean().optional().describe("Traverse Jira issue links (1 hop) to surface context from connected issues \u2014 blockers, related work, cancelled items (default false)")
27090
27828
  },
27091
27829
  async (args) => {
27092
27830
  const jira = createJiraClient(jiraUserConfig);
@@ -27099,6 +27837,7 @@ function createJiraTools(store, projectConfig) {
27099
27837
  sprintId: args.sprintId,
27100
27838
  analyzeComments: args.analyzeComments ?? false,
27101
27839
  applyUpdates: args.applyUpdates ?? false,
27840
+ traverseLinks: args.traverseLinks ?? false,
27102
27841
  statusMap
27103
27842
  }
27104
27843
  );
@@ -27108,6 +27847,34 @@ function createJiraTools(store, projectConfig) {
27108
27847
  };
27109
27848
  },
27110
27849
  { annotations: { readOnlyHint: false } }
27850
+ ),
27851
+ // --- Single-artifact assessment ---
27852
+ tool20(
27853
+ "assess_artifact",
27854
+ "Deep assessment of a single Marvin artifact (task, action, or epic). Fetches live Jira status, analyzes comments with LLM, traverses all linked issues, detects drift, rolls up child progress, and extracts contextual signals (blockers, unblocks, handoffs, superseded work).",
27855
+ {
27856
+ artifactId: external_exports.string().describe("Marvin artifact ID (e.g. 'T-063', 'A-151', 'E-003')"),
27857
+ applyUpdates: external_exports.boolean().optional().describe("Apply proposed status/progress updates to the artifact (default false)")
27858
+ },
27859
+ async (args) => {
27860
+ const jira = createJiraClient(jiraUserConfig);
27861
+ if (!jira) return jiraNotConfiguredError();
27862
+ const report = await assessArtifact(
27863
+ store,
27864
+ jira.client,
27865
+ jira.host,
27866
+ {
27867
+ artifactId: args.artifactId,
27868
+ applyUpdates: args.applyUpdates ?? false,
27869
+ statusMap
27870
+ }
27871
+ );
27872
+ return {
27873
+ content: [{ type: "text", text: formatArtifactReport(report) }],
27874
+ isError: report.errors.length > 0 && report.type === "unknown"
27875
+ };
27876
+ },
27877
+ { annotations: { readOnlyHint: false } }
27111
27878
  )
27112
27879
  ];
27113
27880
  }
@@ -27209,7 +27976,8 @@ var COMMON_TOOLS = `**Available tools:**
27209
27976
  - \`read_confluence_page\` \u2014 **read-only**: fetch and return the content of a Confluence page by URL or page ID. Use this to review Confluence content for updating tasks, generating contributions, or answering questions.
27210
27977
  - \`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.
27211
27978
  - \`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).
27212
- - \`assess_sprint_progress\` \u2014 fetch live Jira statuses for all sprint-scoped items, detect drift, group by focus area with rollup progress, and extract comment signals. Use \`analyzeComments=true\` for LLM summaries, \`applyUpdates=true\` to apply changes.
27979
+ - \`assess_sprint_progress\` \u2014 fetch live Jira statuses for all sprint-scoped items, detect drift, group by focus area with rollup progress, and extract comment signals. Use \`analyzeComments=true\` for LLM summaries, \`applyUpdates=true\` to apply changes, \`traverseLinks=true\` to walk Jira issue links recursively.
27980
+ - \`assess_artifact\` \u2014 deep assessment of a single artifact (task, action, or epic). Fetches Jira status, LLM-summarizes comments, recursively traverses linked issues, detects drift, rolls up child progress, and extracts contextual signals (blockers, unblocks, handoffs, superseded work). Use \`applyUpdates=true\` to apply proposed changes.
27213
27981
  - \`fetch_jira_statuses\` \u2014 **read-only**: discover all Jira statuses in a project and show their Marvin mappings (mapped vs unmapped).
27214
27982
  - \`search_jira\` \u2014 **read-only**: search Jira via JQL and return results with Marvin cross-references. No documents created \u2014 use to preview before importing or find issues for linking.
27215
27983
  - \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import Jira issues as local JI-xxx documents (for Jira-originated items with no existing Marvin artifact).
@@ -27226,6 +27994,11 @@ var COMMON_WORKFLOW = `**Jira sync workflow:**
27226
27994
  2. Review focus area rollups, status drift, and blockers
27227
27995
  3. Optionally run with \`applyUpdates=true\` to bulk-sync statuses, or \`analyzeComments=true\` for LLM-powered comment summaries
27228
27996
 
27997
+ **Single-artifact deep dive:**
27998
+ 1. Call \`assess_artifact\` with an artifact ID to get a focused assessment with Jira sync, comment analysis, link traversal, and child rollup
27999
+ 2. Review signals (blockers, unblocks, handoffs) and proposed updates
28000
+ 3. Use \`applyUpdates=true\` to apply changes
28001
+
27229
28002
  **Daily review workflow:**
27230
28003
  1. Call \`fetch_jira_daily\` (optionally with \`from\`/\`to\` date range) to get a summary of all Jira activity
27231
28004
  2. Review the proposed actions: status updates, unlinked issues to track, questions that may be answered, Confluence pages to review
@@ -27250,6 +28023,7 @@ ${COMMON_WORKFLOW}
27250
28023
  **As Product Owner, use Jira integration to:**
27251
28024
  - Use \`fetch_jira_daily\` for daily standups \u2014 review what changed, identify status drift, spot untracked work
27252
28025
  - Use \`assess_sprint_progress\` for sprint reviews \u2014 see overall progress by focus area, detect drift, and identify blockers
28026
+ - Use \`assess_artifact\` for deep dives into specific features or epics \u2014 full Jira context, linked issues, and child rollup
27253
28027
  - Pull stakeholder-reported issues for triage and prioritization
27254
28028
  - Push approved features as Stories for development tracking
27255
28029
  - Link decisions to Jira issues for audit trail and traceability
@@ -27263,6 +28037,7 @@ ${COMMON_WORKFLOW}
27263
28037
  **As Tech Lead, use Jira integration to:**
27264
28038
  - Use \`fetch_jira_daily\` to review technical progress \u2014 status transitions, new comments, Confluence design docs
27265
28039
  - Use \`assess_sprint_progress\` for sprint health checks \u2014 focus area rollups, Jira drift detection, blocker tracking
28040
+ - Use \`assess_artifact\` to investigate a specific task \u2014 see full dependency chain, comment context, and blocker status
27266
28041
  - Pull technical issues and bugs for sprint planning and estimation
27267
28042
  - Push epics, tasks, and technical decisions to Jira for cross-team visibility
27268
28043
  - Use \`link_to_jira\` to connect Marvin tasks to existing Jira tickets
@@ -27278,6 +28053,7 @@ This is a third path for progress tracking alongside Contributions and Meetings.
27278
28053
  - Use \`fetch_jira_daily\` for daily progress reports \u2014 track what moved, identify blockers, spot untracked work
27279
28054
  - Use \`assess_sprint_progress\` for sprint reviews and stakeholder updates \u2014 comprehensive progress by focus area with Jira enrichment
27280
28055
  - Use \`assess_sprint_progress\` with \`applyUpdates=true\` to bulk-sync Marvin statuses from Jira
28056
+ - Use \`assess_artifact\` for focused status checks on critical items \u2014 full Jira context without running the whole sprint assessment
27281
28057
  - Pull sprint issues for tracking progress and blockers
27282
28058
  - Push actions and tasks to Jira for stakeholder visibility
27283
28059
  - Use \`fetch_jira_daily\` with a date range for sprint retrospectives (e.g. \`from: "2026-03-10", to: "2026-03-21"\`)
@@ -32758,7 +33534,7 @@ function createProgram() {
32758
33534
  const program = new Command();
32759
33535
  program.name("marvin").description(
32760
33536
  "AI-powered product development assistant with Product Owner, Delivery Manager, and Technical Lead personas"
32761
- ).version("0.5.17");
33537
+ ).version("0.5.18");
32762
33538
  program.command("init").description("Initialize a new Marvin project in the current directory").action(async () => {
32763
33539
  await initCommand();
32764
33540
  });