mrvn-cli 0.5.14 → 0.5.16
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.d.ts +5 -1
- package/dist/index.js +168 -36
- package/dist/index.js.map +1 -1
- package/dist/marvin-serve.js +163 -33
- package/dist/marvin-serve.js.map +1 -1
- package/dist/marvin.js +168 -36
- package/dist/marvin.js.map +1 -1
- package/package.json +1 -1
package/dist/marvin-serve.js
CHANGED
|
@@ -19178,24 +19178,53 @@ var DEFAULT_TASK_STATUS_MAP = {
|
|
|
19178
19178
|
blocked: ["Blocked"],
|
|
19179
19179
|
backlog: ["To Do", "Open", "Backlog", "New"]
|
|
19180
19180
|
};
|
|
19181
|
-
function
|
|
19181
|
+
function isConditionalEntry(value) {
|
|
19182
|
+
return !Array.isArray(value) && typeof value === "object" && "default" in value;
|
|
19183
|
+
}
|
|
19184
|
+
function buildStatusLookup(configMap, defaults, inSprint = false) {
|
|
19182
19185
|
const map2 = configMap ?? defaults;
|
|
19183
19186
|
const lookup = /* @__PURE__ */ new Map();
|
|
19184
|
-
for (const [marvinStatus,
|
|
19185
|
-
|
|
19187
|
+
for (const [marvinStatus, value] of Object.entries(map2)) {
|
|
19188
|
+
const statuses = isConditionalEntry(value) ? value.default : value;
|
|
19189
|
+
for (const js of statuses) {
|
|
19186
19190
|
lookup.set(js.toLowerCase(), marvinStatus);
|
|
19187
19191
|
}
|
|
19188
19192
|
}
|
|
19193
|
+
if (inSprint) {
|
|
19194
|
+
for (const [marvinStatus, value] of Object.entries(map2)) {
|
|
19195
|
+
if (isConditionalEntry(value) && value.inSprint) {
|
|
19196
|
+
for (const js of value.inSprint) {
|
|
19197
|
+
lookup.set(js.toLowerCase(), marvinStatus);
|
|
19198
|
+
}
|
|
19199
|
+
}
|
|
19200
|
+
}
|
|
19201
|
+
}
|
|
19189
19202
|
return lookup;
|
|
19190
19203
|
}
|
|
19191
|
-
function mapJiraStatusForAction(status, configMap) {
|
|
19192
|
-
const lookup = buildStatusLookup(configMap, DEFAULT_ACTION_STATUS_MAP);
|
|
19204
|
+
function mapJiraStatusForAction(status, configMap, inSprint) {
|
|
19205
|
+
const lookup = buildStatusLookup(configMap, DEFAULT_ACTION_STATUS_MAP, inSprint ?? false);
|
|
19193
19206
|
return lookup.get(status.toLowerCase()) ?? "open";
|
|
19194
19207
|
}
|
|
19195
|
-
function mapJiraStatusForTask(status, configMap) {
|
|
19196
|
-
const lookup = buildStatusLookup(configMap, DEFAULT_TASK_STATUS_MAP);
|
|
19208
|
+
function mapJiraStatusForTask(status, configMap, inSprint) {
|
|
19209
|
+
const lookup = buildStatusLookup(configMap, DEFAULT_TASK_STATUS_MAP, inSprint ?? false);
|
|
19197
19210
|
return lookup.get(status.toLowerCase()) ?? "backlog";
|
|
19198
19211
|
}
|
|
19212
|
+
function isInActiveSprint(store, tags) {
|
|
19213
|
+
if (!tags) return false;
|
|
19214
|
+
const sprintTags = tags.filter((t) => t.startsWith("sprint:"));
|
|
19215
|
+
if (sprintTags.length === 0) return false;
|
|
19216
|
+
for (const tag of sprintTags) {
|
|
19217
|
+
const sprintId = tag.slice(7);
|
|
19218
|
+
const sprintDoc = store.get(sprintId);
|
|
19219
|
+
if (sprintDoc) {
|
|
19220
|
+
const status = sprintDoc.frontmatter.status;
|
|
19221
|
+
if (status === "active" || status === "completed") {
|
|
19222
|
+
return true;
|
|
19223
|
+
}
|
|
19224
|
+
}
|
|
19225
|
+
}
|
|
19226
|
+
return false;
|
|
19227
|
+
}
|
|
19199
19228
|
function extractJiraKeyFromTags(tags) {
|
|
19200
19229
|
if (!tags) return void 0;
|
|
19201
19230
|
const tag = tags.find((t) => /^jira:[A-Z]+-\d+$/i.test(t));
|
|
@@ -19237,7 +19266,8 @@ async function fetchJiraStatus(store, client, host, artifactId, statusMap) {
|
|
|
19237
19266
|
const artifactType = doc.frontmatter.type;
|
|
19238
19267
|
try {
|
|
19239
19268
|
const issue2 = await client.getIssueWithLinks(jiraKey);
|
|
19240
|
-
const
|
|
19269
|
+
const inSprint = isInActiveSprint(store, doc.frontmatter.tags);
|
|
19270
|
+
const proposedStatus = artifactType === "task" ? mapJiraStatusForTask(issue2.fields.status.name, statusMap?.task, inSprint) : mapJiraStatusForAction(issue2.fields.status.name, statusMap?.action, inSprint);
|
|
19241
19271
|
const currentStatus = doc.frontmatter.status;
|
|
19242
19272
|
const linkedIssues = [];
|
|
19243
19273
|
if (issue2.fields.subtasks) {
|
|
@@ -19549,7 +19579,7 @@ async function fetchJiraDaily(store, client, host, projectKey, dateRange, status
|
|
|
19549
19579
|
const batch = issues.slice(i, i + BATCH_SIZE2);
|
|
19550
19580
|
const results = await Promise.allSettled(
|
|
19551
19581
|
batch.map(
|
|
19552
|
-
(issue2) => processIssue(issue2, client, host, dateRange, jiraKeyToArtifacts, allDocs, statusMap)
|
|
19582
|
+
(issue2) => processIssue(issue2, client, host, dateRange, jiraKeyToArtifacts, allDocs, statusMap, store)
|
|
19553
19583
|
)
|
|
19554
19584
|
);
|
|
19555
19585
|
for (let j = 0; j < results.length; j++) {
|
|
@@ -19566,7 +19596,7 @@ async function fetchJiraDaily(store, client, host, projectKey, dateRange, status
|
|
|
19566
19596
|
summary.proposedActions = generateProposedActions(summary.issues);
|
|
19567
19597
|
return summary;
|
|
19568
19598
|
}
|
|
19569
|
-
async function processIssue(issue2, client, host, dateRange, jiraKeyToArtifacts, allDocs, statusMap) {
|
|
19599
|
+
async function processIssue(issue2, client, host, dateRange, jiraKeyToArtifacts, allDocs, statusMap, store) {
|
|
19570
19600
|
const [changelogResult, commentsResult, remoteLinksResult, issueWithLinks] = await Promise.all([
|
|
19571
19601
|
client.getChangelog(issue2.key).catch(() => []),
|
|
19572
19602
|
client.getComments(issue2.key).catch(() => []),
|
|
@@ -19654,7 +19684,8 @@ async function processIssue(issue2, client, host, dateRange, jiraKeyToArtifacts,
|
|
|
19654
19684
|
if (artifactType === "action" || artifactType === "task") {
|
|
19655
19685
|
const jiraStatus = issue2.fields.status?.name;
|
|
19656
19686
|
if (jiraStatus) {
|
|
19657
|
-
|
|
19687
|
+
const inSprint = store ? isInActiveSprint(store, fm.tags) : false;
|
|
19688
|
+
proposedStatus = artifactType === "task" ? mapJiraStatusForTask(jiraStatus, statusMap?.task, inSprint) : mapJiraStatusForAction(jiraStatus, statusMap?.action, inSprint);
|
|
19658
19689
|
}
|
|
19659
19690
|
}
|
|
19660
19691
|
marvinArtifacts.push({
|
|
@@ -19774,6 +19805,61 @@ function generateProposedActions(issues) {
|
|
|
19774
19805
|
import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
|
|
19775
19806
|
var DONE_STATUSES7 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "obsolete", "wont do", "cancelled"]);
|
|
19776
19807
|
var BATCH_SIZE = 5;
|
|
19808
|
+
var BLOCKED_WEIGHT_RISK_THRESHOLD = 0.3;
|
|
19809
|
+
var COMPLEXITY_WEIGHTS = {
|
|
19810
|
+
trivial: 1,
|
|
19811
|
+
simple: 2,
|
|
19812
|
+
moderate: 3,
|
|
19813
|
+
complex: 5,
|
|
19814
|
+
"very-complex": 8
|
|
19815
|
+
};
|
|
19816
|
+
var DEFAULT_WEIGHT = 3;
|
|
19817
|
+
var STATUS_PROGRESS_DEFAULTS = {
|
|
19818
|
+
done: 100,
|
|
19819
|
+
closed: 100,
|
|
19820
|
+
resolved: 100,
|
|
19821
|
+
obsolete: 100,
|
|
19822
|
+
"wont do": 100,
|
|
19823
|
+
cancelled: 100,
|
|
19824
|
+
review: 80,
|
|
19825
|
+
"in-progress": 40,
|
|
19826
|
+
ready: 5,
|
|
19827
|
+
backlog: 0,
|
|
19828
|
+
open: 0
|
|
19829
|
+
};
|
|
19830
|
+
var BLOCKED_DEFAULT_PROGRESS = 10;
|
|
19831
|
+
function resolveWeight(complexity) {
|
|
19832
|
+
if (complexity && complexity in COMPLEXITY_WEIGHTS) {
|
|
19833
|
+
return { weight: COMPLEXITY_WEIGHTS[complexity], weightSource: "complexity" };
|
|
19834
|
+
}
|
|
19835
|
+
return { weight: DEFAULT_WEIGHT, weightSource: "default" };
|
|
19836
|
+
}
|
|
19837
|
+
function resolveProgress(frontmatter, commentAnalysisProgress) {
|
|
19838
|
+
const hasExplicitProgress = "progress" in frontmatter && typeof frontmatter.progress === "number";
|
|
19839
|
+
if (hasExplicitProgress) {
|
|
19840
|
+
return { progress: Math.max(0, Math.min(100, Math.round(frontmatter.progress))), progressSource: "explicit" };
|
|
19841
|
+
}
|
|
19842
|
+
if (commentAnalysisProgress !== null) {
|
|
19843
|
+
return { progress: Math.max(0, Math.min(100, Math.round(commentAnalysisProgress))), progressSource: "comment-analysis" };
|
|
19844
|
+
}
|
|
19845
|
+
const status = frontmatter.status;
|
|
19846
|
+
if (status === "blocked") {
|
|
19847
|
+
return { progress: BLOCKED_DEFAULT_PROGRESS, progressSource: "status-default" };
|
|
19848
|
+
}
|
|
19849
|
+
const defaultProgress = STATUS_PROGRESS_DEFAULTS[status] ?? 0;
|
|
19850
|
+
return { progress: defaultProgress, progressSource: "status-default" };
|
|
19851
|
+
}
|
|
19852
|
+
function computeWeightedProgress(items) {
|
|
19853
|
+
if (items.length === 0) return 0;
|
|
19854
|
+
let totalWeight = 0;
|
|
19855
|
+
let weightedSum = 0;
|
|
19856
|
+
for (const item of items) {
|
|
19857
|
+
totalWeight += item.weight;
|
|
19858
|
+
weightedSum += item.weight * item.progress;
|
|
19859
|
+
}
|
|
19860
|
+
if (totalWeight === 0) return 0;
|
|
19861
|
+
return Math.round(weightedSum / totalWeight);
|
|
19862
|
+
}
|
|
19777
19863
|
async function assessSprintProgress(store, client, host, options = {}) {
|
|
19778
19864
|
const errors = [];
|
|
19779
19865
|
const sprintData = collectSprintSummaryData(store, options.sprintId);
|
|
@@ -19851,7 +19937,8 @@ async function assessSprintProgress(store, client, host, options = {}) {
|
|
|
19851
19937
|
const commentSignals = [];
|
|
19852
19938
|
if (jiraData) {
|
|
19853
19939
|
jiraStatus = jiraData.issue.fields.status.name;
|
|
19854
|
-
|
|
19940
|
+
const inSprint = isInActiveSprint(store, fm.tags);
|
|
19941
|
+
proposedMarvinStatus = fm.type === "task" ? mapJiraStatusForTask(jiraStatus, options.statusMap?.task, inSprint) : mapJiraStatusForAction(jiraStatus, options.statusMap?.action, inSprint);
|
|
19855
19942
|
const subtasks = jiraData.issue.fields.subtasks ?? [];
|
|
19856
19943
|
if (subtasks.length > 0) {
|
|
19857
19944
|
jiraSubtaskProgress = computeSubtaskProgress(subtasks);
|
|
@@ -19885,12 +19972,18 @@ async function assessSprintProgress(store, client, host, options = {}) {
|
|
|
19885
19972
|
}
|
|
19886
19973
|
const tags = fm.tags ?? [];
|
|
19887
19974
|
const focusTag = tags.find((t) => t.startsWith("focus:"));
|
|
19975
|
+
const { weight, weightSource } = resolveWeight(fm.complexity);
|
|
19976
|
+
const { progress: resolvedProgress, progressSource } = resolveProgress(fm, null);
|
|
19888
19977
|
const report = {
|
|
19889
19978
|
id: fm.id,
|
|
19890
19979
|
title: fm.title,
|
|
19891
19980
|
type: fm.type,
|
|
19892
19981
|
marvinStatus: fm.status,
|
|
19893
19982
|
marvinProgress: currentProgress,
|
|
19983
|
+
progress: resolvedProgress,
|
|
19984
|
+
progressSource,
|
|
19985
|
+
weight,
|
|
19986
|
+
weightSource,
|
|
19894
19987
|
jiraKey,
|
|
19895
19988
|
jiraStatus,
|
|
19896
19989
|
jiraSubtaskProgress,
|
|
@@ -19923,32 +20016,44 @@ async function assessSprintProgress(store, client, host, options = {}) {
|
|
|
19923
20016
|
for (const child of children) childIds.add(child.id);
|
|
19924
20017
|
}
|
|
19925
20018
|
const rootReports = itemReports.filter((r) => !childIds.has(r.id));
|
|
20019
|
+
for (const report of rootReports) {
|
|
20020
|
+
if (report.children.length > 0) {
|
|
20021
|
+
const doc = store.get(report.id);
|
|
20022
|
+
const hasExplicitOverride = doc?.frontmatter.progressOverride;
|
|
20023
|
+
if (!hasExplicitOverride) {
|
|
20024
|
+
report.progress = computeWeightedProgress(report.children);
|
|
20025
|
+
report.progressSource = "status-default";
|
|
20026
|
+
}
|
|
20027
|
+
}
|
|
20028
|
+
}
|
|
19926
20029
|
const focusAreaMap = /* @__PURE__ */ new Map();
|
|
19927
20030
|
for (const report of rootReports) {
|
|
19928
|
-
|
|
19929
|
-
if (!focusAreaMap.has(
|
|
19930
|
-
focusAreaMap.get(
|
|
20031
|
+
if (!report.focusArea) continue;
|
|
20032
|
+
if (!focusAreaMap.has(report.focusArea)) focusAreaMap.set(report.focusArea, []);
|
|
20033
|
+
focusAreaMap.get(report.focusArea).push(report);
|
|
19931
20034
|
}
|
|
19932
20035
|
const focusAreas = [];
|
|
19933
20036
|
for (const [name, items] of focusAreaMap) {
|
|
19934
20037
|
const allFlatItems = items.flatMap((i) => [i, ...i.children]);
|
|
19935
20038
|
const doneCount = allFlatItems.filter((i) => DONE_STATUSES7.has(i.marvinStatus)).length;
|
|
19936
20039
|
const blockedCount = allFlatItems.filter((i) => i.marvinStatus === "blocked").length;
|
|
19937
|
-
const
|
|
20040
|
+
const progress = computeWeightedProgress(items);
|
|
20041
|
+
const totalWeight = items.reduce((s, i) => s + i.weight, 0);
|
|
20042
|
+
const blockedWeight = items.filter((i) => i.marvinStatus === "blocked").reduce((s, i) => s + i.weight, 0);
|
|
20043
|
+
const blockedWeightPct = totalWeight > 0 ? Math.round(blockedWeight / totalWeight * 100) : 0;
|
|
20044
|
+
const riskWarning = blockedWeightPct > BLOCKED_WEIGHT_RISK_THRESHOLD * 100 ? `${blockedWeightPct}% of scope is blocked` : null;
|
|
19938
20045
|
focusAreas.push({
|
|
19939
20046
|
name,
|
|
19940
|
-
|
|
19941
|
-
|
|
20047
|
+
progress,
|
|
20048
|
+
taskCount: allFlatItems.length,
|
|
19942
20049
|
doneCount,
|
|
19943
20050
|
blockedCount,
|
|
19944
|
-
|
|
20051
|
+
blockedWeightPct,
|
|
20052
|
+
riskWarning,
|
|
20053
|
+
items
|
|
19945
20054
|
});
|
|
19946
20055
|
}
|
|
19947
|
-
focusAreas.sort((a, b) =>
|
|
19948
|
-
if (a.name === "Uncategorized") return 1;
|
|
19949
|
-
if (b.name === "Uncategorized") return -1;
|
|
19950
|
-
return a.name.localeCompare(b.name);
|
|
19951
|
-
});
|
|
20056
|
+
focusAreas.sort((a, b) => a.name.localeCompare(b.name));
|
|
19952
20057
|
const driftItems = itemReports.filter((r) => r.statusDrift || r.progressDrift);
|
|
19953
20058
|
const blockers = itemReports.filter(
|
|
19954
20059
|
(r) => r.marvinStatus === "blocked" || r.commentSignals.some((s) => s.type === "blocker")
|
|
@@ -19964,7 +20069,19 @@ async function assessSprintProgress(store, client, host, options = {}) {
|
|
|
19964
20069
|
);
|
|
19965
20070
|
for (const [artifactId, summary] of summaries) {
|
|
19966
20071
|
const report = itemReports.find((r) => r.id === artifactId);
|
|
19967
|
-
if (report)
|
|
20072
|
+
if (report) {
|
|
20073
|
+
report.commentSummary = summary;
|
|
20074
|
+
if (report.progressSource === "status-default") {
|
|
20075
|
+
const pctMatch = summary.match(/(\d{1,3})%/);
|
|
20076
|
+
if (pctMatch) {
|
|
20077
|
+
const pct = parseInt(pctMatch[1], 10);
|
|
20078
|
+
if (pct >= 0 && pct <= 100) {
|
|
20079
|
+
report.progress = pct;
|
|
20080
|
+
report.progressSource = "comment-analysis";
|
|
20081
|
+
}
|
|
20082
|
+
}
|
|
20083
|
+
}
|
|
20084
|
+
}
|
|
19968
20085
|
}
|
|
19969
20086
|
} catch (err) {
|
|
19970
20087
|
errors.push(`Comment analysis failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -20006,7 +20123,7 @@ async function assessSprintProgress(store, client, host, options = {}) {
|
|
|
20006
20123
|
totalDays: sprintData.timeline.totalDays,
|
|
20007
20124
|
percentComplete: sprintData.timeline.percentComplete
|
|
20008
20125
|
},
|
|
20009
|
-
overallProgress: sprintData.workItems.completionPct,
|
|
20126
|
+
overallProgress: rootReports.length > 0 ? computeWeightedProgress(rootReports) : sprintData.workItems.completionPct,
|
|
20010
20127
|
itemReports: rootReports,
|
|
20011
20128
|
focusAreas,
|
|
20012
20129
|
driftItems,
|
|
@@ -20103,9 +20220,12 @@ function formatProgressReport(report) {
|
|
|
20103
20220
|
parts.push(`## Focus Areas`);
|
|
20104
20221
|
parts.push("");
|
|
20105
20222
|
for (const area of report.focusAreas) {
|
|
20106
|
-
const bar = progressBar(area.
|
|
20107
|
-
parts.push(`### ${area.name} ${bar} ${area.
|
|
20108
|
-
parts.push(`${area.doneCount}/${area.
|
|
20223
|
+
const bar = progressBar(area.progress);
|
|
20224
|
+
parts.push(`### ${area.name} ${bar} ${area.progress}%`);
|
|
20225
|
+
parts.push(`${area.doneCount}/${area.taskCount} done${area.blockedCount > 0 ? ` | ${area.blockedCount} blocked` : ""}`);
|
|
20226
|
+
if (area.riskWarning) {
|
|
20227
|
+
parts.push(` \u26A0 ${area.riskWarning}`);
|
|
20228
|
+
}
|
|
20109
20229
|
parts.push("");
|
|
20110
20230
|
for (const item of area.items) {
|
|
20111
20231
|
formatItemLine(parts, item, 0);
|
|
@@ -20169,8 +20289,10 @@ function formatItemLine(parts, item, depth) {
|
|
|
20169
20289
|
const statusIcon = DONE_STATUSES7.has(item.marvinStatus) ? "\u2713" : item.marvinStatus === "blocked" ? "\u{1F6AB}" : item.marvinStatus === "in-progress" ? "\u25B6" : "\u25CB";
|
|
20170
20290
|
const jiraLabel = item.jiraKey ? ` [${item.jiraKey}: ${item.jiraStatus}]` : "";
|
|
20171
20291
|
const driftFlag = item.statusDrift ? " \u26A0drift" : "";
|
|
20172
|
-
const progressLabel =
|
|
20173
|
-
|
|
20292
|
+
const progressLabel = ` ${item.progress}%`;
|
|
20293
|
+
const weightLabel = `w${item.weight}`;
|
|
20294
|
+
const sourceLabel = item.progressSource === "explicit" ? "" : item.progressSource === "comment-analysis" ? " (llm)" : " (est)";
|
|
20295
|
+
parts.push(`${indent}${statusIcon} ${item.id} \u2014 ${item.title} [${item.marvinStatus}]${progressLabel}${sourceLabel} (${weightLabel})${jiraLabel}${driftFlag}`);
|
|
20174
20296
|
if (item.commentSummary) {
|
|
20175
20297
|
parts.push(`${indent} \u{1F4AC} ${item.commentSummary}`);
|
|
20176
20298
|
}
|
|
@@ -20855,12 +20977,20 @@ function createJiraTools(store, projectConfig) {
|
|
|
20855
20977
|
const actionMap = statusMap?.action ?? DEFAULT_ACTION_STATUS_MAP;
|
|
20856
20978
|
const taskMap = statusMap?.task ?? DEFAULT_TASK_STATUS_MAP;
|
|
20857
20979
|
const actionLookup = /* @__PURE__ */ new Map();
|
|
20858
|
-
for (const [marvin,
|
|
20980
|
+
for (const [marvin, value] of Object.entries(actionMap)) {
|
|
20981
|
+
const jiraStatuses = Array.isArray(value) ? value : value.default;
|
|
20859
20982
|
for (const js of jiraStatuses) actionLookup.set(js.toLowerCase(), marvin);
|
|
20983
|
+
if (!Array.isArray(value) && value.inSprint) {
|
|
20984
|
+
for (const js of value.inSprint) actionLookup.set(js.toLowerCase(), `${marvin} (inSprint)`);
|
|
20985
|
+
}
|
|
20860
20986
|
}
|
|
20861
20987
|
const taskLookup = /* @__PURE__ */ new Map();
|
|
20862
|
-
for (const [marvin,
|
|
20988
|
+
for (const [marvin, value] of Object.entries(taskMap)) {
|
|
20989
|
+
const jiraStatuses = Array.isArray(value) ? value : value.default;
|
|
20863
20990
|
for (const js of jiraStatuses) taskLookup.set(js.toLowerCase(), marvin);
|
|
20991
|
+
if (!Array.isArray(value) && value.inSprint) {
|
|
20992
|
+
for (const js of value.inSprint) taskLookup.set(js.toLowerCase(), `${marvin} (inSprint)`);
|
|
20993
|
+
}
|
|
20864
20994
|
}
|
|
20865
20995
|
const parts = [
|
|
20866
20996
|
`Found ${statusCounts.size} distinct statuses in ${resolvedProjectKey} (scanned ${data.issues.length} of ${data.total} issues):`,
|