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 CHANGED
@@ -16,8 +16,12 @@ interface MarvinUserConfig {
16
16
  interface GitConfig {
17
17
  remote?: string;
18
18
  }
19
+ interface ConditionalJiraStatusEntry {
20
+ default: string[];
21
+ inSprint?: string[];
22
+ }
19
23
  interface JiraStatusMap {
20
- [marvinStatus: string]: string[];
24
+ [marvinStatus: string]: string[] | ConditionalJiraStatusEntry;
21
25
  }
22
26
  interface JiraProjectConfig {
23
27
  projectKey?: string;
package/dist/index.js CHANGED
@@ -25092,24 +25092,53 @@ var DEFAULT_TASK_STATUS_MAP = {
25092
25092
  blocked: ["Blocked"],
25093
25093
  backlog: ["To Do", "Open", "Backlog", "New"]
25094
25094
  };
25095
- function buildStatusLookup(configMap, defaults) {
25095
+ function isConditionalEntry(value) {
25096
+ return !Array.isArray(value) && typeof value === "object" && "default" in value;
25097
+ }
25098
+ function buildStatusLookup(configMap, defaults, inSprint = false) {
25096
25099
  const map2 = configMap ?? defaults;
25097
25100
  const lookup = /* @__PURE__ */ new Map();
25098
- for (const [marvinStatus, jiraStatuses] of Object.entries(map2)) {
25099
- for (const js of jiraStatuses) {
25101
+ for (const [marvinStatus, value] of Object.entries(map2)) {
25102
+ const statuses = isConditionalEntry(value) ? value.default : value;
25103
+ for (const js of statuses) {
25100
25104
  lookup.set(js.toLowerCase(), marvinStatus);
25101
25105
  }
25102
25106
  }
25107
+ if (inSprint) {
25108
+ for (const [marvinStatus, value] of Object.entries(map2)) {
25109
+ if (isConditionalEntry(value) && value.inSprint) {
25110
+ for (const js of value.inSprint) {
25111
+ lookup.set(js.toLowerCase(), marvinStatus);
25112
+ }
25113
+ }
25114
+ }
25115
+ }
25103
25116
  return lookup;
25104
25117
  }
25105
- function mapJiraStatusForAction(status, configMap) {
25106
- const lookup = buildStatusLookup(configMap, DEFAULT_ACTION_STATUS_MAP);
25118
+ function mapJiraStatusForAction(status, configMap, inSprint) {
25119
+ const lookup = buildStatusLookup(configMap, DEFAULT_ACTION_STATUS_MAP, inSprint ?? false);
25107
25120
  return lookup.get(status.toLowerCase()) ?? "open";
25108
25121
  }
25109
- function mapJiraStatusForTask(status, configMap) {
25110
- const lookup = buildStatusLookup(configMap, DEFAULT_TASK_STATUS_MAP);
25122
+ function mapJiraStatusForTask(status, configMap, inSprint) {
25123
+ const lookup = buildStatusLookup(configMap, DEFAULT_TASK_STATUS_MAP, inSprint ?? false);
25111
25124
  return lookup.get(status.toLowerCase()) ?? "backlog";
25112
25125
  }
25126
+ function isInActiveSprint(store, tags) {
25127
+ if (!tags) return false;
25128
+ const sprintTags = tags.filter((t) => t.startsWith("sprint:"));
25129
+ if (sprintTags.length === 0) return false;
25130
+ for (const tag of sprintTags) {
25131
+ const sprintId = tag.slice(7);
25132
+ const sprintDoc = store.get(sprintId);
25133
+ if (sprintDoc) {
25134
+ const status = sprintDoc.frontmatter.status;
25135
+ if (status === "active" || status === "completed") {
25136
+ return true;
25137
+ }
25138
+ }
25139
+ }
25140
+ return false;
25141
+ }
25113
25142
  function extractJiraKeyFromTags(tags) {
25114
25143
  if (!tags) return void 0;
25115
25144
  const tag = tags.find((t) => /^jira:[A-Z]+-\d+$/i.test(t));
@@ -25151,7 +25180,8 @@ async function fetchJiraStatus(store, client, host, artifactId, statusMap) {
25151
25180
  const artifactType = doc.frontmatter.type;
25152
25181
  try {
25153
25182
  const issue2 = await client.getIssueWithLinks(jiraKey);
25154
- const proposedStatus = artifactType === "task" ? mapJiraStatusForTask(issue2.fields.status.name, statusMap?.task) : mapJiraStatusForAction(issue2.fields.status.name, statusMap?.action);
25183
+ const inSprint = isInActiveSprint(store, doc.frontmatter.tags);
25184
+ const proposedStatus = artifactType === "task" ? mapJiraStatusForTask(issue2.fields.status.name, statusMap?.task, inSprint) : mapJiraStatusForAction(issue2.fields.status.name, statusMap?.action, inSprint);
25155
25185
  const currentStatus = doc.frontmatter.status;
25156
25186
  const linkedIssues = [];
25157
25187
  if (issue2.fields.subtasks) {
@@ -25503,7 +25533,7 @@ async function fetchJiraDaily(store, client, host, projectKey, dateRange, status
25503
25533
  const batch = issues.slice(i, i + BATCH_SIZE2);
25504
25534
  const results = await Promise.allSettled(
25505
25535
  batch.map(
25506
- (issue2) => processIssue(issue2, client, host, dateRange, jiraKeyToArtifacts, allDocs, statusMap)
25536
+ (issue2) => processIssue(issue2, client, host, dateRange, jiraKeyToArtifacts, allDocs, statusMap, store)
25507
25537
  )
25508
25538
  );
25509
25539
  for (let j = 0; j < results.length; j++) {
@@ -25520,7 +25550,7 @@ async function fetchJiraDaily(store, client, host, projectKey, dateRange, status
25520
25550
  summary.proposedActions = generateProposedActions(summary.issues);
25521
25551
  return summary;
25522
25552
  }
25523
- async function processIssue(issue2, client, host, dateRange, jiraKeyToArtifacts, allDocs, statusMap) {
25553
+ async function processIssue(issue2, client, host, dateRange, jiraKeyToArtifacts, allDocs, statusMap, store) {
25524
25554
  const [changelogResult, commentsResult, remoteLinksResult, issueWithLinks] = await Promise.all([
25525
25555
  client.getChangelog(issue2.key).catch(() => []),
25526
25556
  client.getComments(issue2.key).catch(() => []),
@@ -25608,7 +25638,8 @@ async function processIssue(issue2, client, host, dateRange, jiraKeyToArtifacts,
25608
25638
  if (artifactType === "action" || artifactType === "task") {
25609
25639
  const jiraStatus = issue2.fields.status?.name;
25610
25640
  if (jiraStatus) {
25611
- proposedStatus = artifactType === "task" ? mapJiraStatusForTask(jiraStatus, statusMap?.task) : mapJiraStatusForAction(jiraStatus, statusMap?.action);
25641
+ const inSprint = store ? isInActiveSprint(store, fm.tags) : false;
25642
+ proposedStatus = artifactType === "task" ? mapJiraStatusForTask(jiraStatus, statusMap?.task, inSprint) : mapJiraStatusForAction(jiraStatus, statusMap?.action, inSprint);
25612
25643
  }
25613
25644
  }
25614
25645
  marvinArtifacts.push({
@@ -25728,6 +25759,61 @@ function generateProposedActions(issues) {
25728
25759
  import { query as query3 } from "@anthropic-ai/claude-agent-sdk";
25729
25760
  var DONE_STATUSES16 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "obsolete", "wont do", "cancelled"]);
25730
25761
  var BATCH_SIZE = 5;
25762
+ var BLOCKED_WEIGHT_RISK_THRESHOLD = 0.3;
25763
+ var COMPLEXITY_WEIGHTS = {
25764
+ trivial: 1,
25765
+ simple: 2,
25766
+ moderate: 3,
25767
+ complex: 5,
25768
+ "very-complex": 8
25769
+ };
25770
+ var DEFAULT_WEIGHT = 3;
25771
+ var STATUS_PROGRESS_DEFAULTS = {
25772
+ done: 100,
25773
+ closed: 100,
25774
+ resolved: 100,
25775
+ obsolete: 100,
25776
+ "wont do": 100,
25777
+ cancelled: 100,
25778
+ review: 80,
25779
+ "in-progress": 40,
25780
+ ready: 5,
25781
+ backlog: 0,
25782
+ open: 0
25783
+ };
25784
+ var BLOCKED_DEFAULT_PROGRESS = 10;
25785
+ function resolveWeight(complexity) {
25786
+ if (complexity && complexity in COMPLEXITY_WEIGHTS) {
25787
+ return { weight: COMPLEXITY_WEIGHTS[complexity], weightSource: "complexity" };
25788
+ }
25789
+ return { weight: DEFAULT_WEIGHT, weightSource: "default" };
25790
+ }
25791
+ function resolveProgress(frontmatter, commentAnalysisProgress) {
25792
+ const hasExplicitProgress = "progress" in frontmatter && typeof frontmatter.progress === "number";
25793
+ if (hasExplicitProgress) {
25794
+ return { progress: Math.max(0, Math.min(100, Math.round(frontmatter.progress))), progressSource: "explicit" };
25795
+ }
25796
+ if (commentAnalysisProgress !== null) {
25797
+ return { progress: Math.max(0, Math.min(100, Math.round(commentAnalysisProgress))), progressSource: "comment-analysis" };
25798
+ }
25799
+ const status = frontmatter.status;
25800
+ if (status === "blocked") {
25801
+ return { progress: BLOCKED_DEFAULT_PROGRESS, progressSource: "status-default" };
25802
+ }
25803
+ const defaultProgress = STATUS_PROGRESS_DEFAULTS[status] ?? 0;
25804
+ return { progress: defaultProgress, progressSource: "status-default" };
25805
+ }
25806
+ function computeWeightedProgress(items) {
25807
+ if (items.length === 0) return 0;
25808
+ let totalWeight = 0;
25809
+ let weightedSum = 0;
25810
+ for (const item of items) {
25811
+ totalWeight += item.weight;
25812
+ weightedSum += item.weight * item.progress;
25813
+ }
25814
+ if (totalWeight === 0) return 0;
25815
+ return Math.round(weightedSum / totalWeight);
25816
+ }
25731
25817
  async function assessSprintProgress(store, client, host, options = {}) {
25732
25818
  const errors = [];
25733
25819
  const sprintData = collectSprintSummaryData(store, options.sprintId);
@@ -25805,7 +25891,8 @@ async function assessSprintProgress(store, client, host, options = {}) {
25805
25891
  const commentSignals = [];
25806
25892
  if (jiraData) {
25807
25893
  jiraStatus = jiraData.issue.fields.status.name;
25808
- proposedMarvinStatus = fm.type === "task" ? mapJiraStatusForTask(jiraStatus, options.statusMap?.task) : mapJiraStatusForAction(jiraStatus, options.statusMap?.action);
25894
+ const inSprint = isInActiveSprint(store, fm.tags);
25895
+ proposedMarvinStatus = fm.type === "task" ? mapJiraStatusForTask(jiraStatus, options.statusMap?.task, inSprint) : mapJiraStatusForAction(jiraStatus, options.statusMap?.action, inSprint);
25809
25896
  const subtasks = jiraData.issue.fields.subtasks ?? [];
25810
25897
  if (subtasks.length > 0) {
25811
25898
  jiraSubtaskProgress = computeSubtaskProgress(subtasks);
@@ -25839,12 +25926,18 @@ async function assessSprintProgress(store, client, host, options = {}) {
25839
25926
  }
25840
25927
  const tags = fm.tags ?? [];
25841
25928
  const focusTag = tags.find((t) => t.startsWith("focus:"));
25929
+ const { weight, weightSource } = resolveWeight(fm.complexity);
25930
+ const { progress: resolvedProgress, progressSource } = resolveProgress(fm, null);
25842
25931
  const report = {
25843
25932
  id: fm.id,
25844
25933
  title: fm.title,
25845
25934
  type: fm.type,
25846
25935
  marvinStatus: fm.status,
25847
25936
  marvinProgress: currentProgress,
25937
+ progress: resolvedProgress,
25938
+ progressSource,
25939
+ weight,
25940
+ weightSource,
25848
25941
  jiraKey,
25849
25942
  jiraStatus,
25850
25943
  jiraSubtaskProgress,
@@ -25877,32 +25970,44 @@ async function assessSprintProgress(store, client, host, options = {}) {
25877
25970
  for (const child of children) childIds.add(child.id);
25878
25971
  }
25879
25972
  const rootReports = itemReports.filter((r) => !childIds.has(r.id));
25973
+ for (const report of rootReports) {
25974
+ if (report.children.length > 0) {
25975
+ const doc = store.get(report.id);
25976
+ const hasExplicitOverride = doc?.frontmatter.progressOverride;
25977
+ if (!hasExplicitOverride) {
25978
+ report.progress = computeWeightedProgress(report.children);
25979
+ report.progressSource = "status-default";
25980
+ }
25981
+ }
25982
+ }
25880
25983
  const focusAreaMap = /* @__PURE__ */ new Map();
25881
25984
  for (const report of rootReports) {
25882
- const area = report.focusArea ?? "Uncategorized";
25883
- if (!focusAreaMap.has(area)) focusAreaMap.set(area, []);
25884
- focusAreaMap.get(area).push(report);
25985
+ if (!report.focusArea) continue;
25986
+ if (!focusAreaMap.has(report.focusArea)) focusAreaMap.set(report.focusArea, []);
25987
+ focusAreaMap.get(report.focusArea).push(report);
25885
25988
  }
25886
25989
  const focusAreas = [];
25887
25990
  for (const [name, items] of focusAreaMap) {
25888
25991
  const allFlatItems = items.flatMap((i) => [i, ...i.children]);
25889
25992
  const doneCount = allFlatItems.filter((i) => DONE_STATUSES16.has(i.marvinStatus)).length;
25890
25993
  const blockedCount = allFlatItems.filter((i) => i.marvinStatus === "blocked").length;
25891
- const avgProgress = allFlatItems.length > 0 ? Math.round(allFlatItems.reduce((s, i) => s + i.marvinProgress, 0) / allFlatItems.length) : 0;
25994
+ const progress = computeWeightedProgress(items);
25995
+ const totalWeight = items.reduce((s, i) => s + i.weight, 0);
25996
+ const blockedWeight = items.filter((i) => i.marvinStatus === "blocked").reduce((s, i) => s + i.weight, 0);
25997
+ const blockedWeightPct = totalWeight > 0 ? Math.round(blockedWeight / totalWeight * 100) : 0;
25998
+ const riskWarning = blockedWeightPct > BLOCKED_WEIGHT_RISK_THRESHOLD * 100 ? `${blockedWeightPct}% of scope is blocked` : null;
25892
25999
  focusAreas.push({
25893
26000
  name,
25894
- items,
25895
- totalCount: allFlatItems.length,
26001
+ progress,
26002
+ taskCount: allFlatItems.length,
25896
26003
  doneCount,
25897
26004
  blockedCount,
25898
- avgProgress
26005
+ blockedWeightPct,
26006
+ riskWarning,
26007
+ items
25899
26008
  });
25900
26009
  }
25901
- focusAreas.sort((a, b) => {
25902
- if (a.name === "Uncategorized") return 1;
25903
- if (b.name === "Uncategorized") return -1;
25904
- return a.name.localeCompare(b.name);
25905
- });
26010
+ focusAreas.sort((a, b) => a.name.localeCompare(b.name));
25906
26011
  const driftItems = itemReports.filter((r) => r.statusDrift || r.progressDrift);
25907
26012
  const blockers = itemReports.filter(
25908
26013
  (r) => r.marvinStatus === "blocked" || r.commentSignals.some((s) => s.type === "blocker")
@@ -25918,7 +26023,19 @@ async function assessSprintProgress(store, client, host, options = {}) {
25918
26023
  );
25919
26024
  for (const [artifactId, summary] of summaries) {
25920
26025
  const report = itemReports.find((r) => r.id === artifactId);
25921
- if (report) report.commentSummary = summary;
26026
+ if (report) {
26027
+ report.commentSummary = summary;
26028
+ if (report.progressSource === "status-default") {
26029
+ const pctMatch = summary.match(/(\d{1,3})%/);
26030
+ if (pctMatch) {
26031
+ const pct = parseInt(pctMatch[1], 10);
26032
+ if (pct >= 0 && pct <= 100) {
26033
+ report.progress = pct;
26034
+ report.progressSource = "comment-analysis";
26035
+ }
26036
+ }
26037
+ }
26038
+ }
25922
26039
  }
25923
26040
  } catch (err) {
25924
26041
  errors.push(`Comment analysis failed: ${err instanceof Error ? err.message : String(err)}`);
@@ -25960,7 +26077,7 @@ async function assessSprintProgress(store, client, host, options = {}) {
25960
26077
  totalDays: sprintData.timeline.totalDays,
25961
26078
  percentComplete: sprintData.timeline.percentComplete
25962
26079
  },
25963
- overallProgress: sprintData.workItems.completionPct,
26080
+ overallProgress: rootReports.length > 0 ? computeWeightedProgress(rootReports) : sprintData.workItems.completionPct,
25964
26081
  itemReports: rootReports,
25965
26082
  focusAreas,
25966
26083
  driftItems,
@@ -26057,9 +26174,12 @@ function formatProgressReport(report) {
26057
26174
  parts.push(`## Focus Areas`);
26058
26175
  parts.push("");
26059
26176
  for (const area of report.focusAreas) {
26060
- const bar = progressBar6(area.avgProgress);
26061
- parts.push(`### ${area.name} ${bar} ${area.avgProgress}%`);
26062
- parts.push(`${area.doneCount}/${area.totalCount} done${area.blockedCount > 0 ? ` | ${area.blockedCount} blocked` : ""}`);
26177
+ const bar = progressBar6(area.progress);
26178
+ parts.push(`### ${area.name} ${bar} ${area.progress}%`);
26179
+ parts.push(`${area.doneCount}/${area.taskCount} done${area.blockedCount > 0 ? ` | ${area.blockedCount} blocked` : ""}`);
26180
+ if (area.riskWarning) {
26181
+ parts.push(` \u26A0 ${area.riskWarning}`);
26182
+ }
26063
26183
  parts.push("");
26064
26184
  for (const item of area.items) {
26065
26185
  formatItemLine(parts, item, 0);
@@ -26123,8 +26243,10 @@ function formatItemLine(parts, item, depth) {
26123
26243
  const statusIcon = DONE_STATUSES16.has(item.marvinStatus) ? "\u2713" : item.marvinStatus === "blocked" ? "\u{1F6AB}" : item.marvinStatus === "in-progress" ? "\u25B6" : "\u25CB";
26124
26244
  const jiraLabel = item.jiraKey ? ` [${item.jiraKey}: ${item.jiraStatus}]` : "";
26125
26245
  const driftFlag = item.statusDrift ? " \u26A0drift" : "";
26126
- const progressLabel = item.marvinProgress > 0 ? ` ${item.marvinProgress}%` : "";
26127
- parts.push(`${indent}${statusIcon} ${item.id} \u2014 ${item.title} [${item.marvinStatus}]${progressLabel}${jiraLabel}${driftFlag}`);
26246
+ const progressLabel = ` ${item.progress}%`;
26247
+ const weightLabel = `w${item.weight}`;
26248
+ const sourceLabel = item.progressSource === "explicit" ? "" : item.progressSource === "comment-analysis" ? " (llm)" : " (est)";
26249
+ parts.push(`${indent}${statusIcon} ${item.id} \u2014 ${item.title} [${item.marvinStatus}]${progressLabel}${sourceLabel} (${weightLabel})${jiraLabel}${driftFlag}`);
26128
26250
  if (item.commentSummary) {
26129
26251
  parts.push(`${indent} \u{1F4AC} ${item.commentSummary}`);
26130
26252
  }
@@ -26809,12 +26931,20 @@ function createJiraTools(store, projectConfig) {
26809
26931
  const actionMap = statusMap?.action ?? DEFAULT_ACTION_STATUS_MAP;
26810
26932
  const taskMap = statusMap?.task ?? DEFAULT_TASK_STATUS_MAP;
26811
26933
  const actionLookup = /* @__PURE__ */ new Map();
26812
- for (const [marvin, jiraStatuses] of Object.entries(actionMap)) {
26934
+ for (const [marvin, value] of Object.entries(actionMap)) {
26935
+ const jiraStatuses = Array.isArray(value) ? value : value.default;
26813
26936
  for (const js of jiraStatuses) actionLookup.set(js.toLowerCase(), marvin);
26937
+ if (!Array.isArray(value) && value.inSprint) {
26938
+ for (const js of value.inSprint) actionLookup.set(js.toLowerCase(), `${marvin} (inSprint)`);
26939
+ }
26814
26940
  }
26815
26941
  const taskLookup = /* @__PURE__ */ new Map();
26816
- for (const [marvin, jiraStatuses] of Object.entries(taskMap)) {
26942
+ for (const [marvin, value] of Object.entries(taskMap)) {
26943
+ const jiraStatuses = Array.isArray(value) ? value : value.default;
26817
26944
  for (const js of jiraStatuses) taskLookup.set(js.toLowerCase(), marvin);
26945
+ if (!Array.isArray(value) && value.inSprint) {
26946
+ for (const js of value.inSprint) taskLookup.set(js.toLowerCase(), `${marvin} (inSprint)`);
26947
+ }
26818
26948
  }
26819
26949
  const parts = [
26820
26950
  `Found ${statusCounts.size} distinct statuses in ${resolvedProjectKey} (scanned ${data.issues.length} of ${data.total} issues):`,
@@ -32385,11 +32515,13 @@ async function jiraStatusesCommand(projectKey) {
32385
32515
  statusCounts.set(s, (statusCounts.get(s) ?? 0) + 1);
32386
32516
  }
32387
32517
  const actionLookup = /* @__PURE__ */ new Map();
32388
- for (const [marvin, jiraStatuses] of Object.entries(actionMap)) {
32518
+ for (const [marvin, value] of Object.entries(actionMap)) {
32519
+ const jiraStatuses = Array.isArray(value) ? value : value.default;
32389
32520
  for (const js of jiraStatuses) actionLookup.set(js.toLowerCase(), marvin);
32390
32521
  }
32391
32522
  const taskLookup = /* @__PURE__ */ new Map();
32392
- for (const [marvin, jiraStatuses] of Object.entries(taskMap)) {
32523
+ for (const [marvin, value] of Object.entries(taskMap)) {
32524
+ const jiraStatuses = Array.isArray(value) ? value : value.default;
32393
32525
  for (const js of jiraStatuses) taskLookup.set(js.toLowerCase(), marvin);
32394
32526
  }
32395
32527
  console.log(
@@ -32572,7 +32704,7 @@ function createProgram() {
32572
32704
  const program = new Command();
32573
32705
  program.name("marvin").description(
32574
32706
  "AI-powered product development assistant with Product Owner, Delivery Manager, and Technical Lead personas"
32575
- ).version("0.5.14");
32707
+ ).version("0.5.16");
32576
32708
  program.command("init").description("Initialize a new Marvin project in the current directory").action(async () => {
32577
32709
  await initCommand();
32578
32710
  });