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 +203 -67
- package/dist/index.js.map +1 -1
- package/dist/marvin-serve.js +202 -66
- package/dist/marvin-serve.js.map +1 -1
- package/dist/marvin.js +203 -67
- package/dist/marvin.js.map +1 -1
- package/package.json +25 -2
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 {
|
|
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 {
|
|
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: {
|
|
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: [
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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, [
|
|
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, [
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
27634
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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)
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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.
|
|
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
|
});
|