mrvn-cli 0.5.26 → 0.5.28

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
@@ -22430,7 +22430,10 @@ function normalizeEntry(entry) {
22430
22430
  childCount: typeof entry.childCount === "number" ? entry.childCount : 0,
22431
22431
  childDoneCount: typeof entry.childDoneCount === "number" ? entry.childDoneCount : 0,
22432
22432
  childRollupProgress: typeof entry.childRollupProgress === "number" ? entry.childRollupProgress : null,
22433
- linkedIssueCount: typeof entry.linkedIssueCount === "number" ? entry.linkedIssueCount : 0
22433
+ linkedIssueCount: typeof entry.linkedIssueCount === "number" ? entry.linkedIssueCount : 0,
22434
+ blockerProgress: typeof entry.blockerProgress === "number" ? entry.blockerProgress : null,
22435
+ totalBlockers: typeof entry.totalBlockers === "number" ? entry.totalBlockers : 0,
22436
+ resolvedBlockers: typeof entry.resolvedBlockers === "number" ? entry.resolvedBlockers : 0
22434
22437
  };
22435
22438
  }
22436
22439
  function renderAssessmentTimeline(history) {
@@ -22450,6 +22453,10 @@ function renderAssessmentTimeline(history) {
22450
22453
  const bar = progressBarHtml(entry.childRollupProgress ?? 0);
22451
22454
  parts.push(`<div class="assessment-stat">\u{1F476} Children: ${entry.childDoneCount}/${entry.childCount} done ${bar} ${entry.childRollupProgress ?? 0}%</div>`);
22452
22455
  }
22456
+ if (entry.totalBlockers > 0) {
22457
+ const bar = progressBarHtml(entry.blockerProgress ?? 0);
22458
+ parts.push(`<div class="assessment-stat">\u{1F6A7} Blockers: ${entry.resolvedBlockers}/${entry.totalBlockers} resolved ${bar} ${entry.blockerProgress ?? 0}%</div>`);
22459
+ }
22453
22460
  if (entry.linkedIssueCount > 0) {
22454
22461
  parts.push(`<div class="assessment-stat">\u{1F517} Linked issues: ${entry.linkedIssueCount}</div>`);
22455
22462
  }
@@ -26744,10 +26751,16 @@ function resolveWeight(complexity) {
26744
26751
  function resolveProgress(frontmatter, commentAnalysisProgress) {
26745
26752
  const hasExplicitProgress = "progress" in frontmatter && typeof frontmatter.progress === "number";
26746
26753
  if (hasExplicitProgress) {
26747
- return { progress: Math.max(0, Math.min(100, Math.round(frontmatter.progress))), progressSource: "explicit" };
26754
+ return {
26755
+ progress: Math.max(0, Math.min(100, Math.round(frontmatter.progress))),
26756
+ progressSource: "explicit"
26757
+ };
26748
26758
  }
26749
26759
  if (commentAnalysisProgress !== null) {
26750
- return { progress: Math.max(0, Math.min(100, Math.round(commentAnalysisProgress))), progressSource: "comment-analysis" };
26760
+ return {
26761
+ progress: Math.max(0, Math.min(100, Math.round(commentAnalysisProgress))),
26762
+ progressSource: "comment-analysis"
26763
+ };
26751
26764
  }
26752
26765
  const status = frontmatter.status;
26753
26766
  const defaultProgress = STATUS_PROGRESS_DEFAULTS[status] ?? 0;
@@ -26772,7 +26785,13 @@ async function assessSprintProgress(store, client, host, options = {}) {
26772
26785
  sprintId: options.sprintId ?? "unknown",
26773
26786
  sprintTitle: "Sprint not found",
26774
26787
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
26775
- timeline: { startDate: null, endDate: null, daysRemaining: 0, totalDays: 0, percentComplete: 0 },
26788
+ timeline: {
26789
+ startDate: null,
26790
+ endDate: null,
26791
+ daysRemaining: 0,
26792
+ totalDays: 0,
26793
+ percentComplete: 0
26794
+ },
26776
26795
  overallProgress: 0,
26777
26796
  itemReports: [],
26778
26797
  focusAreas: [],
@@ -26780,7 +26799,9 @@ async function assessSprintProgress(store, client, host, options = {}) {
26780
26799
  blockers: [],
26781
26800
  proposedUpdates: [],
26782
26801
  appliedUpdates: [],
26783
- errors: [`Sprint ${options.sprintId ?? "(active)"} not found. Create a sprint artifact first.`]
26802
+ errors: [
26803
+ `Sprint ${options.sprintId ?? "(active)"} not found. Create a sprint artifact first.`
26804
+ ]
26784
26805
  };
26785
26806
  }
26786
26807
  const sprintTag = `sprint:${sprintData.sprint.id}`;
@@ -26824,7 +26845,9 @@ async function assessSprintProgress(store, client, host, options = {}) {
26824
26845
  });
26825
26846
  } else {
26826
26847
  const batchKey = batch[results.indexOf(result)];
26827
- errors.push(`Failed to fetch ${batchKey}: ${result.reason instanceof Error ? result.reason.message : String(result.reason)}`);
26848
+ errors.push(
26849
+ `Failed to fetch ${batchKey}: ${result.reason instanceof Error ? result.reason.message : String(result.reason)}`
26850
+ );
26828
26851
  }
26829
26852
  }
26830
26853
  }
@@ -26866,12 +26889,16 @@ async function assessSprintProgress(store, client, host, options = {}) {
26866
26889
  }
26867
26890
  } else {
26868
26891
  const batchKey = batch[results.indexOf(result)];
26869
- errors.push(`Failed to fetch linked issue ${batchKey}: ${result.reason instanceof Error ? result.reason.message : String(result.reason)}`);
26892
+ errors.push(
26893
+ `Failed to fetch linked issue ${batchKey}: ${result.reason instanceof Error ? result.reason.message : String(result.reason)}`
26894
+ );
26870
26895
  }
26871
26896
  }
26872
26897
  }
26873
26898
  if (queue.length > 0) {
26874
- errors.push(`Link traversal capped at ${MAX_LINKED_ISSUES} linked issues (${queue.length} remaining undiscovered)`);
26899
+ errors.push(
26900
+ `Link traversal capped at ${MAX_LINKED_ISSUES} linked issues (${queue.length} remaining undiscovered)`
26901
+ );
26875
26902
  }
26876
26903
  }
26877
26904
  const proposedUpdates = [];
@@ -26949,12 +26976,7 @@ async function assessSprintProgress(store, client, host, options = {}) {
26949
26976
  );
26950
26977
  itemLinkedIssues = allLinks;
26951
26978
  itemLinkedIssueSignals.push(...allSignals);
26952
- analyzeLinkedIssueSignals(
26953
- allLinks,
26954
- fm,
26955
- jiraKey,
26956
- proposedUpdates
26957
- );
26979
+ analyzeLinkedIssueSignals(allLinks, fm, jiraKey, proposedUpdates);
26958
26980
  }
26959
26981
  const report = {
26960
26982
  id: fm.id,
@@ -27073,10 +27095,7 @@ async function assessSprintProgress(store, client, host, options = {}) {
27073
27095
  }
27074
27096
  if (options.traverseLinks) {
27075
27097
  try {
27076
- const linkedSummaries = await analyzeLinkedIssueComments(
27077
- itemReports,
27078
- linkedJiraIssues
27079
- );
27098
+ const linkedSummaries = await analyzeLinkedIssueComments(itemReports, linkedJiraIssues);
27080
27099
  for (const [artifactId, signalSummaries] of linkedSummaries) {
27081
27100
  const report = itemReports.find((r) => r.id === artifactId);
27082
27101
  if (!report) continue;
@@ -27088,7 +27107,9 @@ async function assessSprintProgress(store, client, host, options = {}) {
27088
27107
  }
27089
27108
  }
27090
27109
  } catch (err) {
27091
- errors.push(`Linked issue comment analysis failed: ${err instanceof Error ? err.message : String(err)}`);
27110
+ errors.push(
27111
+ `Linked issue comment analysis failed: ${err instanceof Error ? err.message : String(err)}`
27112
+ );
27092
27113
  }
27093
27114
  }
27094
27115
  }
@@ -27160,9 +27181,11 @@ async function analyzeCommentsForProgress(items, jiraIssues, itemJiraKeys) {
27160
27181
  const text = extractCommentText(c.body);
27161
27182
  return ` [${c.author.displayName}, ${c.created.slice(0, 10)}]: ${text.slice(0, 500)}`;
27162
27183
  }).join("\n");
27163
- promptParts.push(`## ${item.id} \u2014 ${item.title} (${jiraKey}, Jira status: ${item.jiraStatus})
27184
+ promptParts.push(
27185
+ `## ${item.id} \u2014 ${item.title} (${jiraKey}, Jira status: ${item.jiraStatus})
27164
27186
  Comments:
27165
- ${commentTexts}`);
27187
+ ${commentTexts}`
27188
+ );
27166
27189
  }
27167
27190
  if (promptParts.length === 0) return summaries;
27168
27191
  const prompt = promptParts.join("\n\n");
@@ -27235,7 +27258,9 @@ function collectTransitiveLinks(primaryIssue, primaryIssues, linkedJiraIssues) {
27235
27258
  commentSummary: null
27236
27259
  });
27237
27260
  }
27238
- const nextLinks = collectLinkedIssues(linkedData.issue).filter((l) => l.relationship !== "subtask" && !visited.has(l.key));
27261
+ const nextLinks = collectLinkedIssues(linkedData.issue).filter(
27262
+ (l) => l.relationship !== "subtask" && !visited.has(l.key)
27263
+ );
27239
27264
  for (const next of nextLinks) {
27240
27265
  visited.add(next.key);
27241
27266
  queue.push(next);
@@ -27259,9 +27284,7 @@ function analyzeLinkedIssueSignals(linkedIssues, frontmatter, jiraKey, proposedU
27259
27284
  reason: `All blocking issues resolved: ${blockerLinks.map((l) => l.key).join(", ")}`
27260
27285
  });
27261
27286
  }
27262
- const wontDoLinks = linkedIssues.filter(
27263
- (l) => WONT_DO_STATUSES.has(l.status.toLowerCase())
27264
- );
27287
+ const wontDoLinks = linkedIssues.filter((l) => WONT_DO_STATUSES.has(l.status.toLowerCase()));
27265
27288
  if (wontDoLinks.length > 0) {
27266
27289
  proposedUpdates.push({
27267
27290
  artifactId: frontmatter.id,
@@ -27272,6 +27295,15 @@ function analyzeLinkedIssueSignals(linkedIssues, frontmatter, jiraKey, proposedU
27272
27295
  });
27273
27296
  }
27274
27297
  }
27298
+ function computeBlockerProgress(linkedIssues, prerequisiteWeight) {
27299
+ const blockerLinks = linkedIssues.filter(
27300
+ (l) => BLOCKER_LINK_PATTERNS.some((p) => l.relationship.toLowerCase().includes(p.split(" ")[0]))
27301
+ );
27302
+ if (blockerLinks.length === 0) return null;
27303
+ const resolved = blockerLinks.filter((l) => l.isDone).length;
27304
+ const blockerProgress = Math.round(resolved / blockerLinks.length * prerequisiteWeight * 100);
27305
+ return { blockerProgress, totalBlockers: blockerLinks.length, resolvedBlockers: resolved };
27306
+ }
27275
27307
  var LINKED_COMMENT_ANALYSIS_PROMPT = `You are a delivery management assistant analyzing Jira comments from linked issues for progress signals.
27276
27308
 
27277
27309
  For each linked issue below, read the comments and produce a 1-sentence summary focused on: impact on the parent issue, blockers, or decisions.
@@ -27326,7 +27358,9 @@ ${linkedParts.join("\n")}`);
27326
27358
  for (const [artifactId, linkedSummaries] of Object.entries(parsed)) {
27327
27359
  if (typeof linkedSummaries === "object" && linkedSummaries !== null) {
27328
27360
  const signalMap = /* @__PURE__ */ new Map();
27329
- for (const [key, summary] of Object.entries(linkedSummaries)) {
27361
+ for (const [key, summary] of Object.entries(
27362
+ linkedSummaries
27363
+ )) {
27330
27364
  if (typeof summary === "string") {
27331
27365
  signalMap.set(key, summary);
27332
27366
  }
@@ -27365,7 +27399,9 @@ function formatProgressReport(report) {
27365
27399
  if (report.timeline.startDate && report.timeline.endDate) {
27366
27400
  parts.push(`## Timeline`);
27367
27401
  parts.push(`${report.timeline.startDate} \u2192 ${report.timeline.endDate}`);
27368
- parts.push(`Days remaining: ${report.timeline.daysRemaining} / ${report.timeline.totalDays} (${report.timeline.percentComplete}% elapsed)`);
27402
+ parts.push(
27403
+ `Days remaining: ${report.timeline.daysRemaining} / ${report.timeline.totalDays} (${report.timeline.percentComplete}% elapsed)`
27404
+ );
27369
27405
  parts.push(`Overall progress: ${report.overallProgress}%`);
27370
27406
  parts.push("");
27371
27407
  }
@@ -27375,7 +27411,9 @@ function formatProgressReport(report) {
27375
27411
  for (const area of report.focusAreas) {
27376
27412
  const bar = progressBar6(area.progress);
27377
27413
  parts.push(`### ${area.name} ${bar} ${area.progress}%`);
27378
- parts.push(`${area.doneCount}/${area.taskCount} done${area.blockedCount > 0 ? ` | ${area.blockedCount} blocked` : ""}`);
27414
+ parts.push(
27415
+ `${area.doneCount}/${area.taskCount} done${area.blockedCount > 0 ? ` | ${area.blockedCount} blocked` : ""}`
27416
+ );
27379
27417
  if (area.riskWarning) {
27380
27418
  parts.push(` \u26A0 ${area.riskWarning}`);
27381
27419
  }
@@ -27414,7 +27452,9 @@ function formatProgressReport(report) {
27414
27452
  if (report.proposedUpdates.length > 0) {
27415
27453
  parts.push(`## Proposed Updates (${report.proposedUpdates.length})`);
27416
27454
  for (const update of report.proposedUpdates) {
27417
- parts.push(` ${update.artifactId}.${update.field}: ${String(update.currentValue)} \u2192 ${String(update.proposedValue)}`);
27455
+ parts.push(
27456
+ ` ${update.artifactId}.${update.field}: ${String(update.currentValue)} \u2192 ${String(update.proposedValue)}`
27457
+ );
27418
27458
  parts.push(` Reason: ${update.reason}`);
27419
27459
  }
27420
27460
  parts.push("");
@@ -27424,7 +27464,9 @@ function formatProgressReport(report) {
27424
27464
  if (report.appliedUpdates.length > 0) {
27425
27465
  parts.push(`## Applied Updates (${report.appliedUpdates.length})`);
27426
27466
  for (const update of report.appliedUpdates) {
27427
- parts.push(` \u2713 ${update.artifactId}.${update.field}: ${String(update.currentValue)} \u2192 ${String(update.proposedValue)}`);
27467
+ parts.push(
27468
+ ` \u2713 ${update.artifactId}.${update.field}: ${String(update.currentValue)} \u2192 ${String(update.proposedValue)}`
27469
+ );
27428
27470
  }
27429
27471
  parts.push("");
27430
27472
  }
@@ -27445,7 +27487,9 @@ function formatItemLine(parts, item, depth) {
27445
27487
  const progressLabel = ` ${item.progress}%`;
27446
27488
  const weightLabel = `w${item.weight}`;
27447
27489
  const sourceLabel = item.progressSource === "explicit" ? "" : item.progressSource === "comment-analysis" ? " (llm)" : " (est)";
27448
- parts.push(`${indent}${statusIcon} ${item.id} \u2014 ${item.title} [${item.marvinStatus}]${progressLabel}${sourceLabel} (${weightLabel})${jiraLabel}${driftFlag}`);
27490
+ parts.push(
27491
+ `${indent}${statusIcon} ${item.id} \u2014 ${item.title} [${item.marvinStatus}]${progressLabel}${sourceLabel} (${weightLabel})${jiraLabel}${driftFlag}`
27492
+ );
27449
27493
  if (item.commentSummary) {
27450
27494
  parts.push(`${indent} \u{1F4AC} ${item.commentSummary}`);
27451
27495
  }
@@ -27455,7 +27499,9 @@ function formatItemLine(parts, item, depth) {
27455
27499
  const doneMarker = link.isDone ? " \u2713" : "";
27456
27500
  const blockerResolved = link.isDone && BLOCKER_LINK_PATTERNS.some((p) => link.relationship.toLowerCase().includes(p.split(" ")[0])) ? " unblock signal" : "";
27457
27501
  const wontDo = WONT_DO_STATUSES.has(link.status.toLowerCase()) ? " \u26A0 needs review" : "";
27458
- parts.push(`${indent} ${link.relationship} ${link.key} "${link.summary}" [${link.status}]${doneMarker}${blockerResolved}${wontDo}`);
27502
+ parts.push(
27503
+ `${indent} ${link.relationship} ${link.key} "${link.summary}" [${link.status}]${doneMarker}${blockerResolved}${wontDo}`
27504
+ );
27459
27505
  const signal = item.linkedIssueSignals.find((s) => s.sourceKey === link.key);
27460
27506
  if (signal?.commentSummary) {
27461
27507
  parts.push(`${indent} \u{1F4AC} ${signal.commentSummary}`);
@@ -27474,6 +27520,7 @@ function progressBar6(pct) {
27474
27520
  var MAX_ARTIFACT_NODES = 50;
27475
27521
  var MAX_LLM_DEPTH = 3;
27476
27522
  var MAX_LLM_COMMENT_CHARS = 8e3;
27523
+ var DEFAULT_PROGRESS_DIVERGENCE_THRESHOLD = 15;
27477
27524
  async function assessArtifact(store, client, host, options) {
27478
27525
  const visited = /* @__PURE__ */ new Set();
27479
27526
  return _assessArtifactRecursive(store, client, host, options, visited, 0);
@@ -27481,10 +27528,14 @@ async function assessArtifact(store, client, host, options) {
27481
27528
  async function _assessArtifactRecursive(store, client, host, options, visited, depth) {
27482
27529
  const errors = [];
27483
27530
  if (visited.has(options.artifactId)) {
27484
- return emptyArtifactReport(options.artifactId, [`Cycle detected: ${options.artifactId} already visited`]);
27531
+ return emptyArtifactReport(options.artifactId, [
27532
+ `Cycle detected: ${options.artifactId} already visited`
27533
+ ]);
27485
27534
  }
27486
27535
  if (visited.size >= MAX_ARTIFACT_NODES) {
27487
- return emptyArtifactReport(options.artifactId, [`Node cap reached (${MAX_ARTIFACT_NODES}), skipping ${options.artifactId}`]);
27536
+ return emptyArtifactReport(options.artifactId, [
27537
+ `Node cap reached (${MAX_ARTIFACT_NODES}), skipping ${options.artifactId}`
27538
+ ]);
27488
27539
  }
27489
27540
  visited.add(options.artifactId);
27490
27541
  const doc = store.get(options.artifactId);
@@ -27554,27 +27605,29 @@ async function _assessArtifactRecursive(store, client, host, options, visited, d
27554
27605
  if (result.status === "fulfilled") {
27555
27606
  const { key, issue: li, comments: lc } = result.value;
27556
27607
  linkedJiraIssues.set(key, { issue: li, comments: lc });
27557
- const newLinks = collectLinkedIssues(li).filter((l) => l.relationship !== "subtask" && !jiraVisited.has(l.key));
27608
+ const newLinks = collectLinkedIssues(li).filter(
27609
+ (l) => l.relationship !== "subtask" && !jiraVisited.has(l.key)
27610
+ );
27558
27611
  for (const nl of newLinks) {
27559
27612
  jiraVisited.add(nl.key);
27560
27613
  queue.push(nl.key);
27561
27614
  }
27562
27615
  } else {
27563
27616
  const batchKey = batch[results.indexOf(result)];
27564
- errors.push(`Failed to fetch linked issue ${batchKey}: ${result.reason instanceof Error ? result.reason.message : String(result.reason)}`);
27617
+ errors.push(
27618
+ `Failed to fetch linked issue ${batchKey}: ${result.reason instanceof Error ? result.reason.message : String(result.reason)}`
27619
+ );
27565
27620
  }
27566
27621
  }
27567
27622
  }
27568
- const { allLinks, allSignals } = collectTransitiveLinks(
27569
- issue2,
27570
- jiraIssues,
27571
- linkedJiraIssues
27572
- );
27623
+ const { allLinks, allSignals } = collectTransitiveLinks(issue2, jiraIssues, linkedJiraIssues);
27573
27624
  linkedIssues = allLinks;
27574
27625
  linkedIssueSignals = allSignals;
27575
27626
  analyzeLinkedIssueSignals(allLinks, fm, jiraKey, proposedUpdates);
27576
27627
  } catch (err) {
27577
- errors.push(`Failed to fetch ${jiraKey}: ${err instanceof Error ? err.message : String(err)}`);
27628
+ errors.push(
27629
+ `Failed to fetch ${jiraKey}: ${err instanceof Error ? err.message : String(err)}`
27630
+ );
27578
27631
  }
27579
27632
  }
27580
27633
  const currentProgress = getEffectiveProgress(fm);
@@ -27615,7 +27668,11 @@ async function _assessArtifactRecursive(store, client, host, options, visited, d
27615
27668
  const primaryHasComments = jiraKey ? (jiraIssues.get(jiraKey)?.comments.length ?? 0) > 0 : false;
27616
27669
  let commentAnalysisProgress = null;
27617
27670
  if (depth < MAX_LLM_DEPTH && jiraKey && primaryHasComments) {
27618
- const estimatedChars = estimateCommentTextSize(jiraIssues, linkedJiraIssues, linkedIssueSignals);
27671
+ const estimatedChars = estimateCommentTextSize(
27672
+ jiraIssues,
27673
+ linkedJiraIssues,
27674
+ linkedIssueSignals
27675
+ );
27619
27676
  if (estimatedChars <= MAX_LLM_COMMENT_CHARS) {
27620
27677
  try {
27621
27678
  const analysis = await analyzeSingleArtifactComments(
@@ -27630,14 +27687,16 @@ async function _assessArtifactRecursive(store, client, host, options, visited, d
27630
27687
  commentSummary = analysis.summary;
27631
27688
  commentAnalysisProgress = analysis.progressEstimate;
27632
27689
  if (commentAnalysisProgress !== null) {
27633
- const hasExplicitProgress = "progress" in fm && typeof fm.progress === "number";
27634
- if (!hasExplicitProgress && !fm.progressOverride && commentAnalysisProgress !== currentProgress) {
27690
+ const divergence = Math.abs(commentAnalysisProgress - currentProgress);
27691
+ const threshold = options.progressDivergenceThreshold ?? DEFAULT_PROGRESS_DIVERGENCE_THRESHOLD;
27692
+ if (divergence >= threshold && commentAnalysisProgress !== currentProgress) {
27693
+ const overrideWarning = fm.progressOverride ? " \u26A0 progressOverride is set \u2014 review before applying" : "";
27635
27694
  proposedUpdates.push({
27636
27695
  artifactId: fm.id,
27637
27696
  field: "progress",
27638
27697
  currentValue: currentProgress,
27639
27698
  proposedValue: commentAnalysisProgress,
27640
- reason: `Comment analysis estimates ${commentAnalysisProgress}% progress`
27699
+ reason: `Comment-derived estimate (${commentAnalysisProgress}%) diverges from current (${currentProgress}%) by ${divergence}pp${overrideWarning}`
27641
27700
  });
27642
27701
  }
27643
27702
  }
@@ -27650,7 +27709,9 @@ async function _assessArtifactRecursive(store, client, host, options, visited, d
27650
27709
  const children = [];
27651
27710
  for (const childId of childIds) {
27652
27711
  if (visited.size >= MAX_ARTIFACT_NODES) {
27653
- errors.push(`Node cap reached (${MAX_ARTIFACT_NODES}), ${childIds.length - children.length} children skipped`);
27712
+ errors.push(
27713
+ `Node cap reached (${MAX_ARTIFACT_NODES}), ${childIds.length - children.length} children skipped`
27714
+ );
27654
27715
  break;
27655
27716
  }
27656
27717
  const childReport = await _assessArtifactRecursive(
@@ -27685,6 +27746,42 @@ async function _assessArtifactRecursive(store, client, host, options, visited, d
27685
27746
  });
27686
27747
  }
27687
27748
  }
27749
+ const prerequisiteWeight = options.prerequisiteWeight ?? 0.3;
27750
+ const blockerResult = computeBlockerProgress(linkedIssues, prerequisiteWeight);
27751
+ let blockerProgressValue = null;
27752
+ let totalBlockersCount = 0;
27753
+ let resolvedBlockersCount = 0;
27754
+ if (blockerResult && !fm.progressOverride && !DONE_STATUSES15.has(fm.status)) {
27755
+ blockerProgressValue = blockerResult.blockerProgress;
27756
+ totalBlockersCount = blockerResult.totalBlockers;
27757
+ resolvedBlockersCount = blockerResult.resolvedBlockers;
27758
+ const lastProgressUpdate = findLast(
27759
+ proposedUpdates,
27760
+ (u) => u.artifactId === fm.id && u.field === "progress"
27761
+ );
27762
+ const implementationProgress = lastProgressUpdate ? lastProgressUpdate.proposedValue : currentProgress;
27763
+ const combinedProgress = Math.round(
27764
+ blockerResult.blockerProgress + implementationProgress * (1 - prerequisiteWeight)
27765
+ );
27766
+ const estimatedProgress = Math.max(currentProgress, combinedProgress);
27767
+ if (estimatedProgress !== currentProgress && estimatedProgress !== implementationProgress) {
27768
+ for (let i = proposedUpdates.length - 1; i >= 0; i--) {
27769
+ if (proposedUpdates[i].artifactId === fm.id && proposedUpdates[i].field === "progress") {
27770
+ proposedUpdates.splice(i, 1);
27771
+ }
27772
+ }
27773
+ proposedUpdates.push({
27774
+ artifactId: fm.id,
27775
+ field: "progress",
27776
+ currentValue: currentProgress,
27777
+ proposedValue: estimatedProgress,
27778
+ reason: `Blocker resolution (${resolvedBlockersCount}/${totalBlockersCount}) + implementation \u2192 dependency-weighted progress ${estimatedProgress}%`
27779
+ });
27780
+ }
27781
+ } else if (blockerResult) {
27782
+ totalBlockersCount = blockerResult.totalBlockers;
27783
+ resolvedBlockersCount = blockerResult.resolvedBlockers;
27784
+ }
27688
27785
  const signals = buildSignals(commentSignals, linkedIssues, statusDrift, proposedMarvinStatus);
27689
27786
  const appliedUpdates = [];
27690
27787
  if (options.applyUpdates && proposedUpdates.length > 0) {
@@ -27727,7 +27824,10 @@ async function _assessArtifactRecursive(store, client, host, options, visited, d
27727
27824
  commentAnalysisProgress,
27728
27825
  signals,
27729
27826
  children,
27730
- linkedIssues
27827
+ linkedIssues,
27828
+ blockerProgressValue,
27829
+ totalBlockersCount,
27830
+ resolvedBlockersCount
27731
27831
  );
27732
27832
  const existingHistory = Array.isArray(fm.assessmentHistory) ? fm.assessmentHistory : [];
27733
27833
  const legacySummary = fm.assessmentSummary;
@@ -27753,7 +27853,9 @@ async function _assessArtifactRecursive(store, client, host, options, visited, d
27753
27853
  }
27754
27854
  store.update(fm.id, payload);
27755
27855
  } catch (err) {
27756
- errors.push(`Failed to persist assessment history: ${err instanceof Error ? err.message : String(err)}`);
27856
+ errors.push(
27857
+ `Failed to persist assessment history: ${err instanceof Error ? err.message : String(err)}`
27858
+ );
27757
27859
  }
27758
27860
  }
27759
27861
  return {
@@ -27776,6 +27878,9 @@ async function _assessArtifactRecursive(store, client, host, options, visited, d
27776
27878
  commentAnalysisProgress,
27777
27879
  linkedIssues,
27778
27880
  linkedIssueSignals,
27881
+ blockerProgress: blockerProgressValue,
27882
+ totalBlockers: totalBlockersCount,
27883
+ resolvedBlockers: resolvedBlockersCount,
27779
27884
  children,
27780
27885
  proposedUpdates: options.applyUpdates ? [] : proposedUpdates,
27781
27886
  appliedUpdates,
@@ -27810,9 +27915,7 @@ function buildSignals(commentSignals, linkedIssues, statusDrift, proposedStatus)
27810
27915
  signals.push(`\u{1F6AB} Blocker \u2014 "${s.snippet}"`);
27811
27916
  }
27812
27917
  }
27813
- const blockingLinks = linkedIssues.filter(
27814
- (l) => l.relationship.toLowerCase().includes("block")
27815
- );
27918
+ const blockingLinks = linkedIssues.filter((l) => l.relationship.toLowerCase().includes("block"));
27816
27919
  const activeBlockers = blockingLinks.filter((l) => !l.isDone);
27817
27920
  const resolvedBlockers = blockingLinks.filter((l) => l.isDone);
27818
27921
  if (activeBlockers.length > 0) {
@@ -27821,7 +27924,9 @@ function buildSignals(commentSignals, linkedIssues, statusDrift, proposedStatus)
27821
27924
  }
27822
27925
  }
27823
27926
  if (resolvedBlockers.length > 0 && activeBlockers.length === 0) {
27824
- signals.push(`\u2705 Unblocked \u2014 all blocking issues resolved: ${resolvedBlockers.map((l) => l.key).join(", ")}`);
27927
+ signals.push(
27928
+ `\u2705 Unblocked \u2014 all blocking issues resolved: ${resolvedBlockers.map((l) => l.key).join(", ")}`
27929
+ );
27825
27930
  }
27826
27931
  const wontDoLinks = linkedIssues.filter((l) => WONT_DO_STATUSES.has(l.status.toLowerCase()));
27827
27932
  for (const l of wontDoLinks) {
@@ -27883,9 +27988,11 @@ async function analyzeSingleArtifactComments(artifactId, title, jiraKey, jiraSta
27883
27988
  const text = extractCommentText(c.body);
27884
27989
  return `[${c.author.displayName}, ${c.created.slice(0, 10)}]: ${text.slice(0, 500)}`;
27885
27990
  }).join("\n");
27886
- promptParts.push(`## ${artifactId} \u2014 ${title} (${jiraKey}, status: ${jiraStatus})
27991
+ promptParts.push(
27992
+ `## ${artifactId} \u2014 ${title} (${jiraKey}, status: ${jiraStatus})
27887
27993
  Comments:
27888
- ${commentTexts}`);
27994
+ ${commentTexts}`
27995
+ );
27889
27996
  }
27890
27997
  for (const signal of linkedIssueSignals) {
27891
27998
  const linkedData = linkedJiraIssues.get(signal.sourceKey);
@@ -27955,6 +28062,9 @@ function emptyArtifactReport(artifactId, errors) {
27955
28062
  commentAnalysisProgress: null,
27956
28063
  linkedIssues: [],
27957
28064
  linkedIssueSignals: [],
28065
+ blockerProgress: null,
28066
+ totalBlockers: 0,
28067
+ resolvedBlockers: 0,
27958
28068
  children: [],
27959
28069
  proposedUpdates: [],
27960
28070
  appliedUpdates: [],
@@ -27962,7 +28072,7 @@ function emptyArtifactReport(artifactId, errors) {
27962
28072
  errors
27963
28073
  };
27964
28074
  }
27965
- function buildAssessmentSummary(commentSummary, commentAnalysisProgress, signals, children, linkedIssues) {
28075
+ function buildAssessmentSummary(commentSummary, commentAnalysisProgress, signals, children, linkedIssues, blockerProgress = null, totalBlockers = 0, resolvedBlockers = 0) {
27966
28076
  const childProgressValues = children.map((c) => {
27967
28077
  const updates = c.appliedUpdates.length > 0 ? c.appliedUpdates : c.proposedUpdates;
27968
28078
  const lastStatus = findLast(updates, (u) => u.field === "status");
@@ -27971,7 +28081,7 @@ function buildAssessmentSummary(commentSummary, commentAnalysisProgress, signals
27971
28081
  if (lastProgress) return lastProgress.proposedValue;
27972
28082
  return c.marvinProgress;
27973
28083
  });
27974
- const childDoneCount = children.filter((c, i) => {
28084
+ const childDoneCount = children.filter((c) => {
27975
28085
  const updates = c.appliedUpdates.length > 0 ? c.appliedUpdates : c.proposedUpdates;
27976
28086
  const lastStatus = findLast(updates, (u) => u.field === "status");
27977
28087
  const effectiveStatus = lastStatus ? String(lastStatus.proposedValue) : c.marvinStatus;
@@ -27986,7 +28096,10 @@ function buildAssessmentSummary(commentSummary, commentAnalysisProgress, signals
27986
28096
  childCount: children.length,
27987
28097
  childDoneCount,
27988
28098
  childRollupProgress,
27989
- linkedIssueCount: linkedIssues.length
28099
+ linkedIssueCount: linkedIssues.length,
28100
+ blockerProgress,
28101
+ totalBlockers,
28102
+ resolvedBlockers
27990
28103
  };
27991
28104
  }
27992
28105
  function formatArtifactReport(report) {
@@ -28004,7 +28117,8 @@ function formatArtifactReport(report) {
28004
28117
  parts.push(`## Jira State (${report.jiraKey})`);
28005
28118
  const jiraParts = [`Status: ${report.jiraStatus ?? "unknown"}`];
28006
28119
  if (report.jiraAssignee) jiraParts.push(`Assignee: ${report.jiraAssignee}`);
28007
- if (report.jiraSubtaskProgress !== null) jiraParts.push(`Subtask progress: ${report.jiraSubtaskProgress}%`);
28120
+ if (report.jiraSubtaskProgress !== null)
28121
+ jiraParts.push(`Subtask progress: ${report.jiraSubtaskProgress}%`);
28008
28122
  parts.push(jiraParts.join(" | "));
28009
28123
  if (report.statusDrift) {
28010
28124
  parts.push(`\u26A0 Drift: ${report.marvinStatus} \u2192 ${report.proposedMarvinStatus}`);
@@ -28022,13 +28136,23 @@ function formatArtifactReport(report) {
28022
28136
  }
28023
28137
  parts.push("");
28024
28138
  }
28139
+ if (report.totalBlockers > 0) {
28140
+ parts.push(`## Blocker Resolution`);
28141
+ const bpLabel = report.blockerProgress !== null ? `${report.blockerProgress}%` : "n/a (skipped)";
28142
+ parts.push(
28143
+ ` ${report.resolvedBlockers}/${report.totalBlockers} blockers resolved \u2192 ${bpLabel} prerequisite progress`
28144
+ );
28145
+ parts.push("");
28146
+ }
28025
28147
  if (report.children.length > 0) {
28026
28148
  const doneCount = report.children.filter((c) => DONE_STATUSES15.has(c.marvinStatus)).length;
28027
28149
  const childProgress = Math.round(
28028
28150
  report.children.reduce((s, c) => s + c.marvinProgress, 0) / report.children.length
28029
28151
  );
28030
28152
  const bar = progressBar6(childProgress);
28031
- parts.push(`## Children (${doneCount}/${report.children.length} done) ${bar} ${childProgress}%`);
28153
+ parts.push(
28154
+ `## Children (${doneCount}/${report.children.length} done) ${bar} ${childProgress}%`
28155
+ );
28032
28156
  for (const child of report.children) {
28033
28157
  formatArtifactChild(parts, child, 1);
28034
28158
  }
@@ -28038,7 +28162,9 @@ function formatArtifactReport(report) {
28038
28162
  parts.push(`## Linked Issues (${report.linkedIssues.length})`);
28039
28163
  for (const link of report.linkedIssues) {
28040
28164
  const doneMarker = link.isDone ? " \u2713" : "";
28041
- parts.push(` ${link.relationship} ${link.key} "${link.summary}" [${link.status}]${doneMarker}`);
28165
+ parts.push(
28166
+ ` ${link.relationship} ${link.key} "${link.summary}" [${link.status}]${doneMarker}`
28167
+ );
28042
28168
  const signal = report.linkedIssueSignals.find((s) => s.sourceKey === link.key);
28043
28169
  if (signal?.commentSummary) {
28044
28170
  parts.push(` \u{1F4AC} ${signal.commentSummary}`);
@@ -28056,7 +28182,9 @@ function formatArtifactReport(report) {
28056
28182
  if (report.proposedUpdates.length > 0) {
28057
28183
  parts.push(`## Proposed Updates (${report.proposedUpdates.length})`);
28058
28184
  for (const update of report.proposedUpdates) {
28059
- parts.push(` ${update.artifactId}.${update.field}: ${String(update.currentValue)} \u2192 ${String(update.proposedValue)}`);
28185
+ parts.push(
28186
+ ` ${update.artifactId}.${update.field}: ${String(update.currentValue)} \u2192 ${String(update.proposedValue)}`
28187
+ );
28060
28188
  parts.push(` Reason: ${update.reason}`);
28061
28189
  }
28062
28190
  parts.push("");
@@ -28066,7 +28194,9 @@ function formatArtifactReport(report) {
28066
28194
  if (report.appliedUpdates.length > 0) {
28067
28195
  parts.push(`## Applied Updates (${report.appliedUpdates.length})`);
28068
28196
  for (const update of report.appliedUpdates) {
28069
- parts.push(` \u2713 ${update.artifactId}.${update.field}: ${String(update.currentValue)} \u2192 ${String(update.proposedValue)}`);
28197
+ parts.push(
28198
+ ` \u2713 ${update.artifactId}.${update.field}: ${String(update.currentValue)} \u2192 ${String(update.proposedValue)}`
28199
+ );
28070
28200
  }
28071
28201
  parts.push("");
28072
28202
  }
@@ -28089,7 +28219,9 @@ function formatArtifactChild(parts, child, depth) {
28089
28219
  if (s.startsWith("\u2705 No active")) continue;
28090
28220
  signalHints.push(s);
28091
28221
  }
28092
- parts.push(`${indent}${icon} ${child.artifactId} \u2014 ${child.title} [${child.marvinStatus}] ${child.marvinProgress}%${jiraLabel}${driftLabel}`);
28222
+ parts.push(
28223
+ `${indent}${icon} ${child.artifactId} \u2014 ${child.title} [${child.marvinStatus}] ${child.marvinProgress}%${jiraLabel}${driftLabel}`
28224
+ );
28093
28225
  if (child.commentSummary) {
28094
28226
  parts.push(`${indent} \u{1F4AC} ${child.commentSummary}`);
28095
28227
  }
@@ -28920,10 +29052,12 @@ function createJiraTools(store, projectConfig) {
28920
29052
  // --- Single-artifact assessment ---
28921
29053
  tool20(
28922
29054
  "assess_artifact",
28923
- "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).",
29055
+ "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, computes dependency-weighted progress from blocker resolution, and extracts contextual signals (blockers, unblocks, handoffs, superseded work).",
28924
29056
  {
28925
29057
  artifactId: external_exports.string().describe("Marvin artifact ID (e.g. 'T-063', 'A-151', 'E-003')"),
28926
- applyUpdates: external_exports.boolean().optional().describe("Apply proposed status/progress updates to the artifact (default false)")
29058
+ applyUpdates: external_exports.boolean().optional().describe("Apply proposed status/progress updates to the artifact (default false)"),
29059
+ prerequisiteWeight: external_exports.number().min(0).max(1).optional().describe("Weight for blocker-resolution progress signal (0-1, default 0.3). Portion of effort attributed to dependency readiness."),
29060
+ progressDivergenceThreshold: external_exports.number().min(0).max(100).optional().describe("Minimum divergence in percentage points between comment-derived progress estimate and stored progress to trigger a proposal (default 15).")
28927
29061
  },
28928
29062
  async (args) => {
28929
29063
  const jira = createJiraClient(jiraUserConfig);
@@ -28935,6 +29069,8 @@ function createJiraTools(store, projectConfig) {
28935
29069
  {
28936
29070
  artifactId: args.artifactId,
28937
29071
  applyUpdates: args.applyUpdates ?? false,
29072
+ prerequisiteWeight: args.prerequisiteWeight,
29073
+ progressDivergenceThreshold: args.progressDivergenceThreshold,
28938
29074
  statusMap
28939
29075
  }
28940
29076
  );
@@ -34349,7 +34485,7 @@ function createProgram() {
34349
34485
  const program2 = new Command();
34350
34486
  program2.name("marvin").description(
34351
34487
  "AI-powered product development assistant with Product Owner, Delivery Manager, and Technical Lead personas"
34352
- ).version("0.5.26");
34488
+ ).version("0.5.28");
34353
34489
  program2.command("init").description("Initialize a new Marvin project in the current directory").action(async () => {
34354
34490
  await initCommand();
34355
34491
  });