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/index.js CHANGED
@@ -21118,7 +21118,10 @@ function normalizeEntry(entry) {
21118
21118
  childCount: typeof entry.childCount === "number" ? entry.childCount : 0,
21119
21119
  childDoneCount: typeof entry.childDoneCount === "number" ? entry.childDoneCount : 0,
21120
21120
  childRollupProgress: typeof entry.childRollupProgress === "number" ? entry.childRollupProgress : null,
21121
- linkedIssueCount: typeof entry.linkedIssueCount === "number" ? entry.linkedIssueCount : 0
21121
+ linkedIssueCount: typeof entry.linkedIssueCount === "number" ? entry.linkedIssueCount : 0,
21122
+ blockerProgress: typeof entry.blockerProgress === "number" ? entry.blockerProgress : null,
21123
+ totalBlockers: typeof entry.totalBlockers === "number" ? entry.totalBlockers : 0,
21124
+ resolvedBlockers: typeof entry.resolvedBlockers === "number" ? entry.resolvedBlockers : 0
21122
21125
  };
21123
21126
  }
21124
21127
  function renderAssessmentTimeline(history) {
@@ -21138,6 +21141,10 @@ function renderAssessmentTimeline(history) {
21138
21141
  const bar = progressBarHtml(entry.childRollupProgress ?? 0);
21139
21142
  parts.push(`<div class="assessment-stat">\u{1F476} Children: ${entry.childDoneCount}/${entry.childCount} done ${bar} ${entry.childRollupProgress ?? 0}%</div>`);
21140
21143
  }
21144
+ if (entry.totalBlockers > 0) {
21145
+ const bar = progressBarHtml(entry.blockerProgress ?? 0);
21146
+ parts.push(`<div class="assessment-stat">\u{1F6A7} Blockers: ${entry.resolvedBlockers}/${entry.totalBlockers} resolved ${bar} ${entry.blockerProgress ?? 0}%</div>`);
21147
+ }
21141
21148
  if (entry.linkedIssueCount > 0) {
21142
21149
  parts.push(`<div class="assessment-stat">\u{1F517} Linked issues: ${entry.linkedIssueCount}</div>`);
21143
21150
  }
@@ -26500,10 +26507,16 @@ function resolveWeight(complexity) {
26500
26507
  function resolveProgress(frontmatter, commentAnalysisProgress) {
26501
26508
  const hasExplicitProgress = "progress" in frontmatter && typeof frontmatter.progress === "number";
26502
26509
  if (hasExplicitProgress) {
26503
- return { progress: Math.max(0, Math.min(100, Math.round(frontmatter.progress))), progressSource: "explicit" };
26510
+ return {
26511
+ progress: Math.max(0, Math.min(100, Math.round(frontmatter.progress))),
26512
+ progressSource: "explicit"
26513
+ };
26504
26514
  }
26505
26515
  if (commentAnalysisProgress !== null) {
26506
- return { progress: Math.max(0, Math.min(100, Math.round(commentAnalysisProgress))), progressSource: "comment-analysis" };
26516
+ return {
26517
+ progress: Math.max(0, Math.min(100, Math.round(commentAnalysisProgress))),
26518
+ progressSource: "comment-analysis"
26519
+ };
26507
26520
  }
26508
26521
  const status = frontmatter.status;
26509
26522
  const defaultProgress = STATUS_PROGRESS_DEFAULTS[status] ?? 0;
@@ -26528,7 +26541,13 @@ async function assessSprintProgress(store, client, host, options = {}) {
26528
26541
  sprintId: options.sprintId ?? "unknown",
26529
26542
  sprintTitle: "Sprint not found",
26530
26543
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
26531
- timeline: { startDate: null, endDate: null, daysRemaining: 0, totalDays: 0, percentComplete: 0 },
26544
+ timeline: {
26545
+ startDate: null,
26546
+ endDate: null,
26547
+ daysRemaining: 0,
26548
+ totalDays: 0,
26549
+ percentComplete: 0
26550
+ },
26532
26551
  overallProgress: 0,
26533
26552
  itemReports: [],
26534
26553
  focusAreas: [],
@@ -26536,7 +26555,9 @@ async function assessSprintProgress(store, client, host, options = {}) {
26536
26555
  blockers: [],
26537
26556
  proposedUpdates: [],
26538
26557
  appliedUpdates: [],
26539
- errors: [`Sprint ${options.sprintId ?? "(active)"} not found. Create a sprint artifact first.`]
26558
+ errors: [
26559
+ `Sprint ${options.sprintId ?? "(active)"} not found. Create a sprint artifact first.`
26560
+ ]
26540
26561
  };
26541
26562
  }
26542
26563
  const sprintTag = `sprint:${sprintData.sprint.id}`;
@@ -26580,7 +26601,9 @@ async function assessSprintProgress(store, client, host, options = {}) {
26580
26601
  });
26581
26602
  } else {
26582
26603
  const batchKey = batch[results.indexOf(result)];
26583
- errors.push(`Failed to fetch ${batchKey}: ${result.reason instanceof Error ? result.reason.message : String(result.reason)}`);
26604
+ errors.push(
26605
+ `Failed to fetch ${batchKey}: ${result.reason instanceof Error ? result.reason.message : String(result.reason)}`
26606
+ );
26584
26607
  }
26585
26608
  }
26586
26609
  }
@@ -26622,12 +26645,16 @@ async function assessSprintProgress(store, client, host, options = {}) {
26622
26645
  }
26623
26646
  } else {
26624
26647
  const batchKey = batch[results.indexOf(result)];
26625
- errors.push(`Failed to fetch linked issue ${batchKey}: ${result.reason instanceof Error ? result.reason.message : String(result.reason)}`);
26648
+ errors.push(
26649
+ `Failed to fetch linked issue ${batchKey}: ${result.reason instanceof Error ? result.reason.message : String(result.reason)}`
26650
+ );
26626
26651
  }
26627
26652
  }
26628
26653
  }
26629
26654
  if (queue.length > 0) {
26630
- errors.push(`Link traversal capped at ${MAX_LINKED_ISSUES} linked issues (${queue.length} remaining undiscovered)`);
26655
+ errors.push(
26656
+ `Link traversal capped at ${MAX_LINKED_ISSUES} linked issues (${queue.length} remaining undiscovered)`
26657
+ );
26631
26658
  }
26632
26659
  }
26633
26660
  const proposedUpdates = [];
@@ -26705,12 +26732,7 @@ async function assessSprintProgress(store, client, host, options = {}) {
26705
26732
  );
26706
26733
  itemLinkedIssues = allLinks;
26707
26734
  itemLinkedIssueSignals.push(...allSignals);
26708
- analyzeLinkedIssueSignals(
26709
- allLinks,
26710
- fm,
26711
- jiraKey,
26712
- proposedUpdates
26713
- );
26735
+ analyzeLinkedIssueSignals(allLinks, fm, jiraKey, proposedUpdates);
26714
26736
  }
26715
26737
  const report = {
26716
26738
  id: fm.id,
@@ -26829,10 +26851,7 @@ async function assessSprintProgress(store, client, host, options = {}) {
26829
26851
  }
26830
26852
  if (options.traverseLinks) {
26831
26853
  try {
26832
- const linkedSummaries = await analyzeLinkedIssueComments(
26833
- itemReports,
26834
- linkedJiraIssues
26835
- );
26854
+ const linkedSummaries = await analyzeLinkedIssueComments(itemReports, linkedJiraIssues);
26836
26855
  for (const [artifactId, signalSummaries] of linkedSummaries) {
26837
26856
  const report = itemReports.find((r) => r.id === artifactId);
26838
26857
  if (!report) continue;
@@ -26844,7 +26863,9 @@ async function assessSprintProgress(store, client, host, options = {}) {
26844
26863
  }
26845
26864
  }
26846
26865
  } catch (err) {
26847
- errors.push(`Linked issue comment analysis failed: ${err instanceof Error ? err.message : String(err)}`);
26866
+ errors.push(
26867
+ `Linked issue comment analysis failed: ${err instanceof Error ? err.message : String(err)}`
26868
+ );
26848
26869
  }
26849
26870
  }
26850
26871
  }
@@ -26916,9 +26937,11 @@ async function analyzeCommentsForProgress(items, jiraIssues, itemJiraKeys) {
26916
26937
  const text = extractCommentText(c.body);
26917
26938
  return ` [${c.author.displayName}, ${c.created.slice(0, 10)}]: ${text.slice(0, 500)}`;
26918
26939
  }).join("\n");
26919
- promptParts.push(`## ${item.id} \u2014 ${item.title} (${jiraKey}, Jira status: ${item.jiraStatus})
26940
+ promptParts.push(
26941
+ `## ${item.id} \u2014 ${item.title} (${jiraKey}, Jira status: ${item.jiraStatus})
26920
26942
  Comments:
26921
- ${commentTexts}`);
26943
+ ${commentTexts}`
26944
+ );
26922
26945
  }
26923
26946
  if (promptParts.length === 0) return summaries;
26924
26947
  const prompt = promptParts.join("\n\n");
@@ -26991,7 +27014,9 @@ function collectTransitiveLinks(primaryIssue, primaryIssues, linkedJiraIssues) {
26991
27014
  commentSummary: null
26992
27015
  });
26993
27016
  }
26994
- const nextLinks = collectLinkedIssues(linkedData.issue).filter((l) => l.relationship !== "subtask" && !visited.has(l.key));
27017
+ const nextLinks = collectLinkedIssues(linkedData.issue).filter(
27018
+ (l) => l.relationship !== "subtask" && !visited.has(l.key)
27019
+ );
26995
27020
  for (const next of nextLinks) {
26996
27021
  visited.add(next.key);
26997
27022
  queue.push(next);
@@ -27015,9 +27040,7 @@ function analyzeLinkedIssueSignals(linkedIssues, frontmatter, jiraKey, proposedU
27015
27040
  reason: `All blocking issues resolved: ${blockerLinks.map((l) => l.key).join(", ")}`
27016
27041
  });
27017
27042
  }
27018
- const wontDoLinks = linkedIssues.filter(
27019
- (l) => WONT_DO_STATUSES.has(l.status.toLowerCase())
27020
- );
27043
+ const wontDoLinks = linkedIssues.filter((l) => WONT_DO_STATUSES.has(l.status.toLowerCase()));
27021
27044
  if (wontDoLinks.length > 0) {
27022
27045
  proposedUpdates.push({
27023
27046
  artifactId: frontmatter.id,
@@ -27028,6 +27051,15 @@ function analyzeLinkedIssueSignals(linkedIssues, frontmatter, jiraKey, proposedU
27028
27051
  });
27029
27052
  }
27030
27053
  }
27054
+ function computeBlockerProgress(linkedIssues, prerequisiteWeight) {
27055
+ const blockerLinks = linkedIssues.filter(
27056
+ (l) => BLOCKER_LINK_PATTERNS.some((p) => l.relationship.toLowerCase().includes(p.split(" ")[0]))
27057
+ );
27058
+ if (blockerLinks.length === 0) return null;
27059
+ const resolved = blockerLinks.filter((l) => l.isDone).length;
27060
+ const blockerProgress = Math.round(resolved / blockerLinks.length * prerequisiteWeight * 100);
27061
+ return { blockerProgress, totalBlockers: blockerLinks.length, resolvedBlockers: resolved };
27062
+ }
27031
27063
  var LINKED_COMMENT_ANALYSIS_PROMPT = `You are a delivery management assistant analyzing Jira comments from linked issues for progress signals.
27032
27064
 
27033
27065
  For each linked issue below, read the comments and produce a 1-sentence summary focused on: impact on the parent issue, blockers, or decisions.
@@ -27082,7 +27114,9 @@ ${linkedParts.join("\n")}`);
27082
27114
  for (const [artifactId, linkedSummaries] of Object.entries(parsed)) {
27083
27115
  if (typeof linkedSummaries === "object" && linkedSummaries !== null) {
27084
27116
  const signalMap = /* @__PURE__ */ new Map();
27085
- for (const [key, summary] of Object.entries(linkedSummaries)) {
27117
+ for (const [key, summary] of Object.entries(
27118
+ linkedSummaries
27119
+ )) {
27086
27120
  if (typeof summary === "string") {
27087
27121
  signalMap.set(key, summary);
27088
27122
  }
@@ -27121,7 +27155,9 @@ function formatProgressReport(report) {
27121
27155
  if (report.timeline.startDate && report.timeline.endDate) {
27122
27156
  parts.push(`## Timeline`);
27123
27157
  parts.push(`${report.timeline.startDate} \u2192 ${report.timeline.endDate}`);
27124
- parts.push(`Days remaining: ${report.timeline.daysRemaining} / ${report.timeline.totalDays} (${report.timeline.percentComplete}% elapsed)`);
27158
+ parts.push(
27159
+ `Days remaining: ${report.timeline.daysRemaining} / ${report.timeline.totalDays} (${report.timeline.percentComplete}% elapsed)`
27160
+ );
27125
27161
  parts.push(`Overall progress: ${report.overallProgress}%`);
27126
27162
  parts.push("");
27127
27163
  }
@@ -27131,7 +27167,9 @@ function formatProgressReport(report) {
27131
27167
  for (const area of report.focusAreas) {
27132
27168
  const bar = progressBar6(area.progress);
27133
27169
  parts.push(`### ${area.name} ${bar} ${area.progress}%`);
27134
- parts.push(`${area.doneCount}/${area.taskCount} done${area.blockedCount > 0 ? ` | ${area.blockedCount} blocked` : ""}`);
27170
+ parts.push(
27171
+ `${area.doneCount}/${area.taskCount} done${area.blockedCount > 0 ? ` | ${area.blockedCount} blocked` : ""}`
27172
+ );
27135
27173
  if (area.riskWarning) {
27136
27174
  parts.push(` \u26A0 ${area.riskWarning}`);
27137
27175
  }
@@ -27170,7 +27208,9 @@ function formatProgressReport(report) {
27170
27208
  if (report.proposedUpdates.length > 0) {
27171
27209
  parts.push(`## Proposed Updates (${report.proposedUpdates.length})`);
27172
27210
  for (const update of report.proposedUpdates) {
27173
- parts.push(` ${update.artifactId}.${update.field}: ${String(update.currentValue)} \u2192 ${String(update.proposedValue)}`);
27211
+ parts.push(
27212
+ ` ${update.artifactId}.${update.field}: ${String(update.currentValue)} \u2192 ${String(update.proposedValue)}`
27213
+ );
27174
27214
  parts.push(` Reason: ${update.reason}`);
27175
27215
  }
27176
27216
  parts.push("");
@@ -27180,7 +27220,9 @@ function formatProgressReport(report) {
27180
27220
  if (report.appliedUpdates.length > 0) {
27181
27221
  parts.push(`## Applied Updates (${report.appliedUpdates.length})`);
27182
27222
  for (const update of report.appliedUpdates) {
27183
- parts.push(` \u2713 ${update.artifactId}.${update.field}: ${String(update.currentValue)} \u2192 ${String(update.proposedValue)}`);
27223
+ parts.push(
27224
+ ` \u2713 ${update.artifactId}.${update.field}: ${String(update.currentValue)} \u2192 ${String(update.proposedValue)}`
27225
+ );
27184
27226
  }
27185
27227
  parts.push("");
27186
27228
  }
@@ -27201,7 +27243,9 @@ function formatItemLine(parts, item, depth) {
27201
27243
  const progressLabel = ` ${item.progress}%`;
27202
27244
  const weightLabel = `w${item.weight}`;
27203
27245
  const sourceLabel = item.progressSource === "explicit" ? "" : item.progressSource === "comment-analysis" ? " (llm)" : " (est)";
27204
- parts.push(`${indent}${statusIcon} ${item.id} \u2014 ${item.title} [${item.marvinStatus}]${progressLabel}${sourceLabel} (${weightLabel})${jiraLabel}${driftFlag}`);
27246
+ parts.push(
27247
+ `${indent}${statusIcon} ${item.id} \u2014 ${item.title} [${item.marvinStatus}]${progressLabel}${sourceLabel} (${weightLabel})${jiraLabel}${driftFlag}`
27248
+ );
27205
27249
  if (item.commentSummary) {
27206
27250
  parts.push(`${indent} \u{1F4AC} ${item.commentSummary}`);
27207
27251
  }
@@ -27211,7 +27255,9 @@ function formatItemLine(parts, item, depth) {
27211
27255
  const doneMarker = link.isDone ? " \u2713" : "";
27212
27256
  const blockerResolved = link.isDone && BLOCKER_LINK_PATTERNS.some((p) => link.relationship.toLowerCase().includes(p.split(" ")[0])) ? " unblock signal" : "";
27213
27257
  const wontDo = WONT_DO_STATUSES.has(link.status.toLowerCase()) ? " \u26A0 needs review" : "";
27214
- parts.push(`${indent} ${link.relationship} ${link.key} "${link.summary}" [${link.status}]${doneMarker}${blockerResolved}${wontDo}`);
27258
+ parts.push(
27259
+ `${indent} ${link.relationship} ${link.key} "${link.summary}" [${link.status}]${doneMarker}${blockerResolved}${wontDo}`
27260
+ );
27215
27261
  const signal = item.linkedIssueSignals.find((s) => s.sourceKey === link.key);
27216
27262
  if (signal?.commentSummary) {
27217
27263
  parts.push(`${indent} \u{1F4AC} ${signal.commentSummary}`);
@@ -27230,6 +27276,7 @@ function progressBar6(pct) {
27230
27276
  var MAX_ARTIFACT_NODES = 50;
27231
27277
  var MAX_LLM_DEPTH = 3;
27232
27278
  var MAX_LLM_COMMENT_CHARS = 8e3;
27279
+ var DEFAULT_PROGRESS_DIVERGENCE_THRESHOLD = 15;
27233
27280
  async function assessArtifact(store, client, host, options) {
27234
27281
  const visited = /* @__PURE__ */ new Set();
27235
27282
  return _assessArtifactRecursive(store, client, host, options, visited, 0);
@@ -27237,10 +27284,14 @@ async function assessArtifact(store, client, host, options) {
27237
27284
  async function _assessArtifactRecursive(store, client, host, options, visited, depth) {
27238
27285
  const errors = [];
27239
27286
  if (visited.has(options.artifactId)) {
27240
- return emptyArtifactReport(options.artifactId, [`Cycle detected: ${options.artifactId} already visited`]);
27287
+ return emptyArtifactReport(options.artifactId, [
27288
+ `Cycle detected: ${options.artifactId} already visited`
27289
+ ]);
27241
27290
  }
27242
27291
  if (visited.size >= MAX_ARTIFACT_NODES) {
27243
- return emptyArtifactReport(options.artifactId, [`Node cap reached (${MAX_ARTIFACT_NODES}), skipping ${options.artifactId}`]);
27292
+ return emptyArtifactReport(options.artifactId, [
27293
+ `Node cap reached (${MAX_ARTIFACT_NODES}), skipping ${options.artifactId}`
27294
+ ]);
27244
27295
  }
27245
27296
  visited.add(options.artifactId);
27246
27297
  const doc = store.get(options.artifactId);
@@ -27310,27 +27361,29 @@ async function _assessArtifactRecursive(store, client, host, options, visited, d
27310
27361
  if (result.status === "fulfilled") {
27311
27362
  const { key, issue: li, comments: lc } = result.value;
27312
27363
  linkedJiraIssues.set(key, { issue: li, comments: lc });
27313
- const newLinks = collectLinkedIssues(li).filter((l) => l.relationship !== "subtask" && !jiraVisited.has(l.key));
27364
+ const newLinks = collectLinkedIssues(li).filter(
27365
+ (l) => l.relationship !== "subtask" && !jiraVisited.has(l.key)
27366
+ );
27314
27367
  for (const nl of newLinks) {
27315
27368
  jiraVisited.add(nl.key);
27316
27369
  queue.push(nl.key);
27317
27370
  }
27318
27371
  } else {
27319
27372
  const batchKey = batch[results.indexOf(result)];
27320
- errors.push(`Failed to fetch linked issue ${batchKey}: ${result.reason instanceof Error ? result.reason.message : String(result.reason)}`);
27373
+ errors.push(
27374
+ `Failed to fetch linked issue ${batchKey}: ${result.reason instanceof Error ? result.reason.message : String(result.reason)}`
27375
+ );
27321
27376
  }
27322
27377
  }
27323
27378
  }
27324
- const { allLinks, allSignals } = collectTransitiveLinks(
27325
- issue2,
27326
- jiraIssues,
27327
- linkedJiraIssues
27328
- );
27379
+ const { allLinks, allSignals } = collectTransitiveLinks(issue2, jiraIssues, linkedJiraIssues);
27329
27380
  linkedIssues = allLinks;
27330
27381
  linkedIssueSignals = allSignals;
27331
27382
  analyzeLinkedIssueSignals(allLinks, fm, jiraKey, proposedUpdates);
27332
27383
  } catch (err) {
27333
- errors.push(`Failed to fetch ${jiraKey}: ${err instanceof Error ? err.message : String(err)}`);
27384
+ errors.push(
27385
+ `Failed to fetch ${jiraKey}: ${err instanceof Error ? err.message : String(err)}`
27386
+ );
27334
27387
  }
27335
27388
  }
27336
27389
  const currentProgress = getEffectiveProgress(fm);
@@ -27371,7 +27424,11 @@ async function _assessArtifactRecursive(store, client, host, options, visited, d
27371
27424
  const primaryHasComments = jiraKey ? (jiraIssues.get(jiraKey)?.comments.length ?? 0) > 0 : false;
27372
27425
  let commentAnalysisProgress = null;
27373
27426
  if (depth < MAX_LLM_DEPTH && jiraKey && primaryHasComments) {
27374
- const estimatedChars = estimateCommentTextSize(jiraIssues, linkedJiraIssues, linkedIssueSignals);
27427
+ const estimatedChars = estimateCommentTextSize(
27428
+ jiraIssues,
27429
+ linkedJiraIssues,
27430
+ linkedIssueSignals
27431
+ );
27375
27432
  if (estimatedChars <= MAX_LLM_COMMENT_CHARS) {
27376
27433
  try {
27377
27434
  const analysis = await analyzeSingleArtifactComments(
@@ -27386,14 +27443,16 @@ async function _assessArtifactRecursive(store, client, host, options, visited, d
27386
27443
  commentSummary = analysis.summary;
27387
27444
  commentAnalysisProgress = analysis.progressEstimate;
27388
27445
  if (commentAnalysisProgress !== null) {
27389
- const hasExplicitProgress = "progress" in fm && typeof fm.progress === "number";
27390
- if (!hasExplicitProgress && !fm.progressOverride && commentAnalysisProgress !== currentProgress) {
27446
+ const divergence = Math.abs(commentAnalysisProgress - currentProgress);
27447
+ const threshold = options.progressDivergenceThreshold ?? DEFAULT_PROGRESS_DIVERGENCE_THRESHOLD;
27448
+ if (divergence >= threshold && commentAnalysisProgress !== currentProgress) {
27449
+ const overrideWarning = fm.progressOverride ? " \u26A0 progressOverride is set \u2014 review before applying" : "";
27391
27450
  proposedUpdates.push({
27392
27451
  artifactId: fm.id,
27393
27452
  field: "progress",
27394
27453
  currentValue: currentProgress,
27395
27454
  proposedValue: commentAnalysisProgress,
27396
- reason: `Comment analysis estimates ${commentAnalysisProgress}% progress`
27455
+ reason: `Comment-derived estimate (${commentAnalysisProgress}%) diverges from current (${currentProgress}%) by ${divergence}pp${overrideWarning}`
27397
27456
  });
27398
27457
  }
27399
27458
  }
@@ -27406,7 +27465,9 @@ async function _assessArtifactRecursive(store, client, host, options, visited, d
27406
27465
  const children = [];
27407
27466
  for (const childId of childIds) {
27408
27467
  if (visited.size >= MAX_ARTIFACT_NODES) {
27409
- errors.push(`Node cap reached (${MAX_ARTIFACT_NODES}), ${childIds.length - children.length} children skipped`);
27468
+ errors.push(
27469
+ `Node cap reached (${MAX_ARTIFACT_NODES}), ${childIds.length - children.length} children skipped`
27470
+ );
27410
27471
  break;
27411
27472
  }
27412
27473
  const childReport = await _assessArtifactRecursive(
@@ -27441,6 +27502,42 @@ async function _assessArtifactRecursive(store, client, host, options, visited, d
27441
27502
  });
27442
27503
  }
27443
27504
  }
27505
+ const prerequisiteWeight = options.prerequisiteWeight ?? 0.3;
27506
+ const blockerResult = computeBlockerProgress(linkedIssues, prerequisiteWeight);
27507
+ let blockerProgressValue = null;
27508
+ let totalBlockersCount = 0;
27509
+ let resolvedBlockersCount = 0;
27510
+ if (blockerResult && !fm.progressOverride && !DONE_STATUSES15.has(fm.status)) {
27511
+ blockerProgressValue = blockerResult.blockerProgress;
27512
+ totalBlockersCount = blockerResult.totalBlockers;
27513
+ resolvedBlockersCount = blockerResult.resolvedBlockers;
27514
+ const lastProgressUpdate = findLast(
27515
+ proposedUpdates,
27516
+ (u) => u.artifactId === fm.id && u.field === "progress"
27517
+ );
27518
+ const implementationProgress = lastProgressUpdate ? lastProgressUpdate.proposedValue : currentProgress;
27519
+ const combinedProgress = Math.round(
27520
+ blockerResult.blockerProgress + implementationProgress * (1 - prerequisiteWeight)
27521
+ );
27522
+ const estimatedProgress = Math.max(currentProgress, combinedProgress);
27523
+ if (estimatedProgress !== currentProgress && estimatedProgress !== implementationProgress) {
27524
+ for (let i = proposedUpdates.length - 1; i >= 0; i--) {
27525
+ if (proposedUpdates[i].artifactId === fm.id && proposedUpdates[i].field === "progress") {
27526
+ proposedUpdates.splice(i, 1);
27527
+ }
27528
+ }
27529
+ proposedUpdates.push({
27530
+ artifactId: fm.id,
27531
+ field: "progress",
27532
+ currentValue: currentProgress,
27533
+ proposedValue: estimatedProgress,
27534
+ reason: `Blocker resolution (${resolvedBlockersCount}/${totalBlockersCount}) + implementation \u2192 dependency-weighted progress ${estimatedProgress}%`
27535
+ });
27536
+ }
27537
+ } else if (blockerResult) {
27538
+ totalBlockersCount = blockerResult.totalBlockers;
27539
+ resolvedBlockersCount = blockerResult.resolvedBlockers;
27540
+ }
27444
27541
  const signals = buildSignals(commentSignals, linkedIssues, statusDrift, proposedMarvinStatus);
27445
27542
  const appliedUpdates = [];
27446
27543
  if (options.applyUpdates && proposedUpdates.length > 0) {
@@ -27483,7 +27580,10 @@ async function _assessArtifactRecursive(store, client, host, options, visited, d
27483
27580
  commentAnalysisProgress,
27484
27581
  signals,
27485
27582
  children,
27486
- linkedIssues
27583
+ linkedIssues,
27584
+ blockerProgressValue,
27585
+ totalBlockersCount,
27586
+ resolvedBlockersCount
27487
27587
  );
27488
27588
  const existingHistory = Array.isArray(fm.assessmentHistory) ? fm.assessmentHistory : [];
27489
27589
  const legacySummary = fm.assessmentSummary;
@@ -27509,7 +27609,9 @@ async function _assessArtifactRecursive(store, client, host, options, visited, d
27509
27609
  }
27510
27610
  store.update(fm.id, payload);
27511
27611
  } catch (err) {
27512
- errors.push(`Failed to persist assessment history: ${err instanceof Error ? err.message : String(err)}`);
27612
+ errors.push(
27613
+ `Failed to persist assessment history: ${err instanceof Error ? err.message : String(err)}`
27614
+ );
27513
27615
  }
27514
27616
  }
27515
27617
  return {
@@ -27532,6 +27634,9 @@ async function _assessArtifactRecursive(store, client, host, options, visited, d
27532
27634
  commentAnalysisProgress,
27533
27635
  linkedIssues,
27534
27636
  linkedIssueSignals,
27637
+ blockerProgress: blockerProgressValue,
27638
+ totalBlockers: totalBlockersCount,
27639
+ resolvedBlockers: resolvedBlockersCount,
27535
27640
  children,
27536
27641
  proposedUpdates: options.applyUpdates ? [] : proposedUpdates,
27537
27642
  appliedUpdates,
@@ -27566,9 +27671,7 @@ function buildSignals(commentSignals, linkedIssues, statusDrift, proposedStatus)
27566
27671
  signals.push(`\u{1F6AB} Blocker \u2014 "${s.snippet}"`);
27567
27672
  }
27568
27673
  }
27569
- const blockingLinks = linkedIssues.filter(
27570
- (l) => l.relationship.toLowerCase().includes("block")
27571
- );
27674
+ const blockingLinks = linkedIssues.filter((l) => l.relationship.toLowerCase().includes("block"));
27572
27675
  const activeBlockers = blockingLinks.filter((l) => !l.isDone);
27573
27676
  const resolvedBlockers = blockingLinks.filter((l) => l.isDone);
27574
27677
  if (activeBlockers.length > 0) {
@@ -27577,7 +27680,9 @@ function buildSignals(commentSignals, linkedIssues, statusDrift, proposedStatus)
27577
27680
  }
27578
27681
  }
27579
27682
  if (resolvedBlockers.length > 0 && activeBlockers.length === 0) {
27580
- signals.push(`\u2705 Unblocked \u2014 all blocking issues resolved: ${resolvedBlockers.map((l) => l.key).join(", ")}`);
27683
+ signals.push(
27684
+ `\u2705 Unblocked \u2014 all blocking issues resolved: ${resolvedBlockers.map((l) => l.key).join(", ")}`
27685
+ );
27581
27686
  }
27582
27687
  const wontDoLinks = linkedIssues.filter((l) => WONT_DO_STATUSES.has(l.status.toLowerCase()));
27583
27688
  for (const l of wontDoLinks) {
@@ -27639,9 +27744,11 @@ async function analyzeSingleArtifactComments(artifactId, title, jiraKey, jiraSta
27639
27744
  const text = extractCommentText(c.body);
27640
27745
  return `[${c.author.displayName}, ${c.created.slice(0, 10)}]: ${text.slice(0, 500)}`;
27641
27746
  }).join("\n");
27642
- promptParts.push(`## ${artifactId} \u2014 ${title} (${jiraKey}, status: ${jiraStatus})
27747
+ promptParts.push(
27748
+ `## ${artifactId} \u2014 ${title} (${jiraKey}, status: ${jiraStatus})
27643
27749
  Comments:
27644
- ${commentTexts}`);
27750
+ ${commentTexts}`
27751
+ );
27645
27752
  }
27646
27753
  for (const signal of linkedIssueSignals) {
27647
27754
  const linkedData = linkedJiraIssues.get(signal.sourceKey);
@@ -27711,6 +27818,9 @@ function emptyArtifactReport(artifactId, errors) {
27711
27818
  commentAnalysisProgress: null,
27712
27819
  linkedIssues: [],
27713
27820
  linkedIssueSignals: [],
27821
+ blockerProgress: null,
27822
+ totalBlockers: 0,
27823
+ resolvedBlockers: 0,
27714
27824
  children: [],
27715
27825
  proposedUpdates: [],
27716
27826
  appliedUpdates: [],
@@ -27718,7 +27828,7 @@ function emptyArtifactReport(artifactId, errors) {
27718
27828
  errors
27719
27829
  };
27720
27830
  }
27721
- function buildAssessmentSummary(commentSummary, commentAnalysisProgress, signals, children, linkedIssues) {
27831
+ function buildAssessmentSummary(commentSummary, commentAnalysisProgress, signals, children, linkedIssues, blockerProgress = null, totalBlockers = 0, resolvedBlockers = 0) {
27722
27832
  const childProgressValues = children.map((c) => {
27723
27833
  const updates = c.appliedUpdates.length > 0 ? c.appliedUpdates : c.proposedUpdates;
27724
27834
  const lastStatus = findLast(updates, (u) => u.field === "status");
@@ -27727,7 +27837,7 @@ function buildAssessmentSummary(commentSummary, commentAnalysisProgress, signals
27727
27837
  if (lastProgress) return lastProgress.proposedValue;
27728
27838
  return c.marvinProgress;
27729
27839
  });
27730
- const childDoneCount = children.filter((c, i) => {
27840
+ const childDoneCount = children.filter((c) => {
27731
27841
  const updates = c.appliedUpdates.length > 0 ? c.appliedUpdates : c.proposedUpdates;
27732
27842
  const lastStatus = findLast(updates, (u) => u.field === "status");
27733
27843
  const effectiveStatus = lastStatus ? String(lastStatus.proposedValue) : c.marvinStatus;
@@ -27742,7 +27852,10 @@ function buildAssessmentSummary(commentSummary, commentAnalysisProgress, signals
27742
27852
  childCount: children.length,
27743
27853
  childDoneCount,
27744
27854
  childRollupProgress,
27745
- linkedIssueCount: linkedIssues.length
27855
+ linkedIssueCount: linkedIssues.length,
27856
+ blockerProgress,
27857
+ totalBlockers,
27858
+ resolvedBlockers
27746
27859
  };
27747
27860
  }
27748
27861
  function formatArtifactReport(report) {
@@ -27760,7 +27873,8 @@ function formatArtifactReport(report) {
27760
27873
  parts.push(`## Jira State (${report.jiraKey})`);
27761
27874
  const jiraParts = [`Status: ${report.jiraStatus ?? "unknown"}`];
27762
27875
  if (report.jiraAssignee) jiraParts.push(`Assignee: ${report.jiraAssignee}`);
27763
- if (report.jiraSubtaskProgress !== null) jiraParts.push(`Subtask progress: ${report.jiraSubtaskProgress}%`);
27876
+ if (report.jiraSubtaskProgress !== null)
27877
+ jiraParts.push(`Subtask progress: ${report.jiraSubtaskProgress}%`);
27764
27878
  parts.push(jiraParts.join(" | "));
27765
27879
  if (report.statusDrift) {
27766
27880
  parts.push(`\u26A0 Drift: ${report.marvinStatus} \u2192 ${report.proposedMarvinStatus}`);
@@ -27778,13 +27892,23 @@ function formatArtifactReport(report) {
27778
27892
  }
27779
27893
  parts.push("");
27780
27894
  }
27895
+ if (report.totalBlockers > 0) {
27896
+ parts.push(`## Blocker Resolution`);
27897
+ const bpLabel = report.blockerProgress !== null ? `${report.blockerProgress}%` : "n/a (skipped)";
27898
+ parts.push(
27899
+ ` ${report.resolvedBlockers}/${report.totalBlockers} blockers resolved \u2192 ${bpLabel} prerequisite progress`
27900
+ );
27901
+ parts.push("");
27902
+ }
27781
27903
  if (report.children.length > 0) {
27782
27904
  const doneCount = report.children.filter((c) => DONE_STATUSES15.has(c.marvinStatus)).length;
27783
27905
  const childProgress = Math.round(
27784
27906
  report.children.reduce((s, c) => s + c.marvinProgress, 0) / report.children.length
27785
27907
  );
27786
27908
  const bar = progressBar6(childProgress);
27787
- parts.push(`## Children (${doneCount}/${report.children.length} done) ${bar} ${childProgress}%`);
27909
+ parts.push(
27910
+ `## Children (${doneCount}/${report.children.length} done) ${bar} ${childProgress}%`
27911
+ );
27788
27912
  for (const child of report.children) {
27789
27913
  formatArtifactChild(parts, child, 1);
27790
27914
  }
@@ -27794,7 +27918,9 @@ function formatArtifactReport(report) {
27794
27918
  parts.push(`## Linked Issues (${report.linkedIssues.length})`);
27795
27919
  for (const link of report.linkedIssues) {
27796
27920
  const doneMarker = link.isDone ? " \u2713" : "";
27797
- parts.push(` ${link.relationship} ${link.key} "${link.summary}" [${link.status}]${doneMarker}`);
27921
+ parts.push(
27922
+ ` ${link.relationship} ${link.key} "${link.summary}" [${link.status}]${doneMarker}`
27923
+ );
27798
27924
  const signal = report.linkedIssueSignals.find((s) => s.sourceKey === link.key);
27799
27925
  if (signal?.commentSummary) {
27800
27926
  parts.push(` \u{1F4AC} ${signal.commentSummary}`);
@@ -27812,7 +27938,9 @@ function formatArtifactReport(report) {
27812
27938
  if (report.proposedUpdates.length > 0) {
27813
27939
  parts.push(`## Proposed Updates (${report.proposedUpdates.length})`);
27814
27940
  for (const update of report.proposedUpdates) {
27815
- parts.push(` ${update.artifactId}.${update.field}: ${String(update.currentValue)} \u2192 ${String(update.proposedValue)}`);
27941
+ parts.push(
27942
+ ` ${update.artifactId}.${update.field}: ${String(update.currentValue)} \u2192 ${String(update.proposedValue)}`
27943
+ );
27816
27944
  parts.push(` Reason: ${update.reason}`);
27817
27945
  }
27818
27946
  parts.push("");
@@ -27822,7 +27950,9 @@ function formatArtifactReport(report) {
27822
27950
  if (report.appliedUpdates.length > 0) {
27823
27951
  parts.push(`## Applied Updates (${report.appliedUpdates.length})`);
27824
27952
  for (const update of report.appliedUpdates) {
27825
- parts.push(` \u2713 ${update.artifactId}.${update.field}: ${String(update.currentValue)} \u2192 ${String(update.proposedValue)}`);
27953
+ parts.push(
27954
+ ` \u2713 ${update.artifactId}.${update.field}: ${String(update.currentValue)} \u2192 ${String(update.proposedValue)}`
27955
+ );
27826
27956
  }
27827
27957
  parts.push("");
27828
27958
  }
@@ -27845,7 +27975,9 @@ function formatArtifactChild(parts, child, depth) {
27845
27975
  if (s.startsWith("\u2705 No active")) continue;
27846
27976
  signalHints.push(s);
27847
27977
  }
27848
- parts.push(`${indent}${icon} ${child.artifactId} \u2014 ${child.title} [${child.marvinStatus}] ${child.marvinProgress}%${jiraLabel}${driftLabel}`);
27978
+ parts.push(
27979
+ `${indent}${icon} ${child.artifactId} \u2014 ${child.title} [${child.marvinStatus}] ${child.marvinProgress}%${jiraLabel}${driftLabel}`
27980
+ );
27849
27981
  if (child.commentSummary) {
27850
27982
  parts.push(`${indent} \u{1F4AC} ${child.commentSummary}`);
27851
27983
  }
@@ -28676,10 +28808,12 @@ function createJiraTools(store, projectConfig) {
28676
28808
  // --- Single-artifact assessment ---
28677
28809
  tool20(
28678
28810
  "assess_artifact",
28679
- "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).",
28811
+ "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).",
28680
28812
  {
28681
28813
  artifactId: external_exports.string().describe("Marvin artifact ID (e.g. 'T-063', 'A-151', 'E-003')"),
28682
- applyUpdates: external_exports.boolean().optional().describe("Apply proposed status/progress updates to the artifact (default false)")
28814
+ applyUpdates: external_exports.boolean().optional().describe("Apply proposed status/progress updates to the artifact (default false)"),
28815
+ 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."),
28816
+ 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).")
28683
28817
  },
28684
28818
  async (args) => {
28685
28819
  const jira = createJiraClient(jiraUserConfig);
@@ -28691,6 +28825,8 @@ function createJiraTools(store, projectConfig) {
28691
28825
  {
28692
28826
  artifactId: args.artifactId,
28693
28827
  applyUpdates: args.applyUpdates ?? false,
28828
+ prerequisiteWeight: args.prerequisiteWeight,
28829
+ progressDivergenceThreshold: args.progressDivergenceThreshold,
28694
28830
  statusMap
28695
28831
  }
28696
28832
  );
@@ -34359,7 +34495,7 @@ function createProgram() {
34359
34495
  const program = new Command();
34360
34496
  program.name("marvin").description(
34361
34497
  "AI-powered product development assistant with Product Owner, Delivery Manager, and Technical Lead personas"
34362
- ).version("0.5.26");
34498
+ ).version("0.5.28");
34363
34499
  program.command("init").description("Initialize a new Marvin project in the current directory").action(async () => {
34364
34500
  await initCommand();
34365
34501
  });