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/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 {
|
|
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 {
|
|
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: {
|
|
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: [
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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, [
|
|
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, [
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
27390
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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)
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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.
|
|
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
|
});
|