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.
@@ -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 buildStatusLookup(configMap, defaults) {
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, jiraStatuses] of Object.entries(map2)) {
19185
- for (const js of jiraStatuses) {
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 proposedStatus = artifactType === "task" ? mapJiraStatusForTask(issue2.fields.status.name, statusMap?.task) : mapJiraStatusForAction(issue2.fields.status.name, statusMap?.action);
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
- proposedStatus = artifactType === "task" ? mapJiraStatusForTask(jiraStatus, statusMap?.task) : mapJiraStatusForAction(jiraStatus, statusMap?.action);
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
- proposedMarvinStatus = fm.type === "task" ? mapJiraStatusForTask(jiraStatus, options.statusMap?.task) : mapJiraStatusForAction(jiraStatus, options.statusMap?.action);
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
- const area = report.focusArea ?? "Uncategorized";
19929
- if (!focusAreaMap.has(area)) focusAreaMap.set(area, []);
19930
- focusAreaMap.get(area).push(report);
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 avgProgress = allFlatItems.length > 0 ? Math.round(allFlatItems.reduce((s, i) => s + i.marvinProgress, 0) / allFlatItems.length) : 0;
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
- items,
19941
- totalCount: allFlatItems.length,
20047
+ progress,
20048
+ taskCount: allFlatItems.length,
19942
20049
  doneCount,
19943
20050
  blockedCount,
19944
- avgProgress
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) report.commentSummary = summary;
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.avgProgress);
20107
- parts.push(`### ${area.name} ${bar} ${area.avgProgress}%`);
20108
- parts.push(`${area.doneCount}/${area.totalCount} done${area.blockedCount > 0 ? ` | ${area.blockedCount} blocked` : ""}`);
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 = item.marvinProgress > 0 ? ` ${item.marvinProgress}%` : "";
20173
- parts.push(`${indent}${statusIcon} ${item.id} \u2014 ${item.title} [${item.marvinStatus}]${progressLabel}${jiraLabel}${driftFlag}`);
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, jiraStatuses] of Object.entries(actionMap)) {
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, jiraStatuses] of Object.entries(taskMap)) {
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):`,