mrvn-cli 0.5.15 → 0.5.17

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.
@@ -14349,11 +14349,25 @@ import { tool as tool2 } from "@anthropic-ai/claude-agent-sdk";
14349
14349
 
14350
14350
  // src/storage/progress.ts
14351
14351
  var DONE_STATUSES = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
14352
+ var STATUS_PROGRESS_DEFAULTS = {
14353
+ done: 100,
14354
+ closed: 100,
14355
+ resolved: 100,
14356
+ obsolete: 100,
14357
+ "wont do": 100,
14358
+ cancelled: 100,
14359
+ review: 80,
14360
+ "in-progress": 40,
14361
+ ready: 5,
14362
+ blocked: 10,
14363
+ backlog: 0,
14364
+ open: 0
14365
+ };
14352
14366
  function getEffectiveProgress(frontmatter) {
14353
14367
  if (DONE_STATUSES.has(frontmatter.status)) return 100;
14354
14368
  const raw = frontmatter.progress;
14355
14369
  if (typeof raw === "number") return Math.max(0, Math.min(100, Math.round(raw)));
14356
- return 0;
14370
+ return STATUS_PROGRESS_DEFAULTS[frontmatter.status] ?? 0;
14357
14371
  }
14358
14372
  function propagateProgressFromTask(store, taskId) {
14359
14373
  const updated = [];
@@ -19178,24 +19192,80 @@ var DEFAULT_TASK_STATUS_MAP = {
19178
19192
  blocked: ["Blocked"],
19179
19193
  backlog: ["To Do", "Open", "Backlog", "New"]
19180
19194
  };
19181
- function buildStatusLookup(configMap, defaults) {
19182
- const map2 = configMap ?? defaults;
19195
+ function isLegacyFormat(statusMap) {
19196
+ if (!statusMap || typeof statusMap !== "object") return false;
19197
+ const keys = Object.keys(statusMap);
19198
+ if (!keys.every((k) => k === "action" || k === "task")) return false;
19199
+ for (const key of keys) {
19200
+ const val = statusMap[key];
19201
+ if (typeof val !== "object" || val === null) return false;
19202
+ for (const innerVal of Object.values(val)) {
19203
+ if (!Array.isArray(innerVal)) return false;
19204
+ if (!innerVal.every((v) => typeof v === "string")) return false;
19205
+ }
19206
+ }
19207
+ return true;
19208
+ }
19209
+ function buildLegacyLookup(legacyMap) {
19183
19210
  const lookup = /* @__PURE__ */ new Map();
19184
- for (const [marvinStatus, jiraStatuses] of Object.entries(map2)) {
19211
+ for (const [marvinStatus, jiraStatuses] of Object.entries(legacyMap)) {
19185
19212
  for (const js of jiraStatuses) {
19186
19213
  lookup.set(js.toLowerCase(), marvinStatus);
19187
19214
  }
19188
19215
  }
19189
19216
  return lookup;
19190
19217
  }
19191
- function mapJiraStatusForAction(status, configMap) {
19192
- const lookup = buildStatusLookup(configMap, DEFAULT_ACTION_STATUS_MAP);
19218
+ function buildFlatLookup(flatMap, inSprint) {
19219
+ const lookup = /* @__PURE__ */ new Map();
19220
+ for (const [jiraStatus, value] of Object.entries(flatMap)) {
19221
+ if (typeof value === "string") {
19222
+ lookup.set(jiraStatus.toLowerCase(), value);
19223
+ } else {
19224
+ const resolved = inSprint && value.inSprint ? value.inSprint : value.default;
19225
+ lookup.set(jiraStatus.toLowerCase(), resolved);
19226
+ }
19227
+ }
19228
+ return lookup;
19229
+ }
19230
+ function normalizeStatusMap(statusMap) {
19231
+ if (!statusMap) return {};
19232
+ if (isLegacyFormat(statusMap)) {
19233
+ return { legacy: statusMap };
19234
+ }
19235
+ return { flat: statusMap };
19236
+ }
19237
+ function mapJiraStatusForAction(status, resolved, inSprint = false) {
19238
+ if (resolved.flat) {
19239
+ const lookup2 = buildFlatLookup(resolved.flat, inSprint);
19240
+ return lookup2.get(status.toLowerCase()) ?? "open";
19241
+ }
19242
+ const lookup = buildLegacyLookup(resolved.legacy?.action ?? DEFAULT_ACTION_STATUS_MAP);
19193
19243
  return lookup.get(status.toLowerCase()) ?? "open";
19194
19244
  }
19195
- function mapJiraStatusForTask(status, configMap) {
19196
- const lookup = buildStatusLookup(configMap, DEFAULT_TASK_STATUS_MAP);
19245
+ function mapJiraStatusForTask(status, resolved, inSprint = false) {
19246
+ if (resolved.flat) {
19247
+ const lookup2 = buildFlatLookup(resolved.flat, inSprint);
19248
+ return lookup2.get(status.toLowerCase()) ?? "backlog";
19249
+ }
19250
+ const lookup = buildLegacyLookup(resolved.legacy?.task ?? DEFAULT_TASK_STATUS_MAP);
19197
19251
  return lookup.get(status.toLowerCase()) ?? "backlog";
19198
19252
  }
19253
+ function isInActiveSprint(store, tags) {
19254
+ if (!tags) return false;
19255
+ const sprintTags = tags.filter((t) => t.startsWith("sprint:"));
19256
+ if (sprintTags.length === 0) return false;
19257
+ for (const tag of sprintTags) {
19258
+ const sprintId = tag.slice(7);
19259
+ const sprintDoc = store.get(sprintId);
19260
+ if (sprintDoc) {
19261
+ const status = sprintDoc.frontmatter.status;
19262
+ if (status === "active" || status === "completed") {
19263
+ return true;
19264
+ }
19265
+ }
19266
+ }
19267
+ return false;
19268
+ }
19199
19269
  function extractJiraKeyFromTags(tags) {
19200
19270
  if (!tags) return void 0;
19201
19271
  const tag = tags.find((t) => /^jira:[A-Z]+-\d+$/i.test(t));
@@ -19237,7 +19307,9 @@ async function fetchJiraStatus(store, client, host, artifactId, statusMap) {
19237
19307
  const artifactType = doc.frontmatter.type;
19238
19308
  try {
19239
19309
  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);
19310
+ const inSprint = isInActiveSprint(store, doc.frontmatter.tags);
19311
+ const resolved = statusMap ?? {};
19312
+ const proposedStatus = artifactType === "task" ? mapJiraStatusForTask(issue2.fields.status.name, resolved, inSprint) : mapJiraStatusForAction(issue2.fields.status.name, resolved, inSprint);
19241
19313
  const currentStatus = doc.frontmatter.status;
19242
19314
  const linkedIssues = [];
19243
19315
  if (issue2.fields.subtasks) {
@@ -19549,7 +19621,7 @@ async function fetchJiraDaily(store, client, host, projectKey, dateRange, status
19549
19621
  const batch = issues.slice(i, i + BATCH_SIZE2);
19550
19622
  const results = await Promise.allSettled(
19551
19623
  batch.map(
19552
- (issue2) => processIssue(issue2, client, host, dateRange, jiraKeyToArtifacts, allDocs, statusMap)
19624
+ (issue2) => processIssue(issue2, client, host, dateRange, jiraKeyToArtifacts, allDocs, statusMap, store)
19553
19625
  )
19554
19626
  );
19555
19627
  for (let j = 0; j < results.length; j++) {
@@ -19566,7 +19638,7 @@ async function fetchJiraDaily(store, client, host, projectKey, dateRange, status
19566
19638
  summary.proposedActions = generateProposedActions(summary.issues);
19567
19639
  return summary;
19568
19640
  }
19569
- async function processIssue(issue2, client, host, dateRange, jiraKeyToArtifacts, allDocs, statusMap) {
19641
+ async function processIssue(issue2, client, host, dateRange, jiraKeyToArtifacts, allDocs, statusMap, store) {
19570
19642
  const [changelogResult, commentsResult, remoteLinksResult, issueWithLinks] = await Promise.all([
19571
19643
  client.getChangelog(issue2.key).catch(() => []),
19572
19644
  client.getComments(issue2.key).catch(() => []),
@@ -19654,7 +19726,9 @@ async function processIssue(issue2, client, host, dateRange, jiraKeyToArtifacts,
19654
19726
  if (artifactType === "action" || artifactType === "task") {
19655
19727
  const jiraStatus = issue2.fields.status?.name;
19656
19728
  if (jiraStatus) {
19657
- proposedStatus = artifactType === "task" ? mapJiraStatusForTask(jiraStatus, statusMap?.task) : mapJiraStatusForAction(jiraStatus, statusMap?.action);
19729
+ const inSprint = store ? isInActiveSprint(store, fm.tags) : false;
19730
+ const resolved = statusMap ?? {};
19731
+ proposedStatus = artifactType === "task" ? mapJiraStatusForTask(jiraStatus, resolved, inSprint) : mapJiraStatusForAction(jiraStatus, resolved, inSprint);
19658
19732
  }
19659
19733
  }
19660
19734
  marvinArtifacts.push({
@@ -19783,20 +19857,6 @@ var COMPLEXITY_WEIGHTS = {
19783
19857
  "very-complex": 8
19784
19858
  };
19785
19859
  var DEFAULT_WEIGHT = 3;
19786
- var STATUS_PROGRESS_DEFAULTS = {
19787
- done: 100,
19788
- closed: 100,
19789
- resolved: 100,
19790
- obsolete: 100,
19791
- "wont do": 100,
19792
- cancelled: 100,
19793
- review: 80,
19794
- "in-progress": 40,
19795
- ready: 5,
19796
- backlog: 0,
19797
- open: 0
19798
- };
19799
- var BLOCKED_DEFAULT_PROGRESS = 10;
19800
19860
  function resolveWeight(complexity) {
19801
19861
  if (complexity && complexity in COMPLEXITY_WEIGHTS) {
19802
19862
  return { weight: COMPLEXITY_WEIGHTS[complexity], weightSource: "complexity" };
@@ -19812,9 +19872,6 @@ function resolveProgress(frontmatter, commentAnalysisProgress) {
19812
19872
  return { progress: Math.max(0, Math.min(100, Math.round(commentAnalysisProgress))), progressSource: "comment-analysis" };
19813
19873
  }
19814
19874
  const status = frontmatter.status;
19815
- if (status === "blocked") {
19816
- return { progress: BLOCKED_DEFAULT_PROGRESS, progressSource: "status-default" };
19817
- }
19818
19875
  const defaultProgress = STATUS_PROGRESS_DEFAULTS[status] ?? 0;
19819
19876
  return { progress: defaultProgress, progressSource: "status-default" };
19820
19877
  }
@@ -19906,7 +19963,9 @@ async function assessSprintProgress(store, client, host, options = {}) {
19906
19963
  const commentSignals = [];
19907
19964
  if (jiraData) {
19908
19965
  jiraStatus = jiraData.issue.fields.status.name;
19909
- proposedMarvinStatus = fm.type === "task" ? mapJiraStatusForTask(jiraStatus, options.statusMap?.task) : mapJiraStatusForAction(jiraStatus, options.statusMap?.action);
19966
+ const inSprint = isInActiveSprint(store, fm.tags);
19967
+ const resolved = options.statusMap ?? {};
19968
+ proposedMarvinStatus = fm.type === "task" ? mapJiraStatusForTask(jiraStatus, resolved, inSprint) : mapJiraStatusForAction(jiraStatus, resolved, inSprint);
19910
19969
  const subtasks = jiraData.issue.fields.subtasks ?? [];
19911
19970
  if (subtasks.length > 0) {
19912
19971
  jiraSubtaskProgress = computeSubtaskProgress(subtasks);
@@ -19937,6 +19996,20 @@ async function assessSprintProgress(store, client, host, options = {}) {
19937
19996
  proposedValue: jiraSubtaskProgress,
19938
19997
  reason: `Jira ${jiraKey} subtask progress is ${jiraSubtaskProgress}%`
19939
19998
  });
19999
+ } else if (statusDrift && proposedMarvinStatus && !fm.progressOverride) {
20000
+ const hasExplicitProgress = "progress" in fm && typeof fm.progress === "number";
20001
+ if (!hasExplicitProgress) {
20002
+ const proposedProgress = STATUS_PROGRESS_DEFAULTS[proposedMarvinStatus] ?? 0;
20003
+ if (proposedProgress !== currentProgress) {
20004
+ proposedUpdates.push({
20005
+ artifactId: fm.id,
20006
+ field: "progress",
20007
+ currentValue: currentProgress,
20008
+ proposedValue: proposedProgress,
20009
+ reason: `Status changing to "${proposedMarvinStatus}" \u2192 default progress ${proposedProgress}%`
20010
+ });
20011
+ }
20012
+ }
19940
20013
  }
19941
20014
  const tags = fm.tags ?? [];
19942
20015
  const focusTag = tags.find((t) => t.startsWith("focus:"));
@@ -20315,7 +20388,7 @@ function findByJiraKey(store, jiraKey) {
20315
20388
  function createJiraTools(store, projectConfig) {
20316
20389
  const jiraUserConfig = loadUserConfig().jira;
20317
20390
  const defaultProjectKey = projectConfig?.jira?.projectKey;
20318
- const statusMap = projectConfig?.jira?.statusMap;
20391
+ const statusMap = normalizeStatusMap(projectConfig?.jira?.statusMap);
20319
20392
  return [
20320
20393
  // --- Local read tools ---
20321
20394
  tool20(
@@ -20942,15 +21015,32 @@ function createJiraTools(store, projectConfig) {
20942
21015
  const s = issue2.fields.status.name;
20943
21016
  statusCounts.set(s, (statusCounts.get(s) ?? 0) + 1);
20944
21017
  }
20945
- const actionMap = statusMap?.action ?? DEFAULT_ACTION_STATUS_MAP;
20946
- const taskMap = statusMap?.task ?? DEFAULT_TASK_STATUS_MAP;
20947
21018
  const actionLookup = /* @__PURE__ */ new Map();
20948
- for (const [marvin, jiraStatuses] of Object.entries(actionMap)) {
20949
- for (const js of jiraStatuses) actionLookup.set(js.toLowerCase(), marvin);
20950
- }
20951
21019
  const taskLookup = /* @__PURE__ */ new Map();
20952
- for (const [marvin, jiraStatuses] of Object.entries(taskMap)) {
20953
- for (const js of jiraStatuses) taskLookup.set(js.toLowerCase(), marvin);
21020
+ if (statusMap.flat) {
21021
+ for (const [jiraStatus, value] of Object.entries(statusMap.flat)) {
21022
+ const lower = jiraStatus.toLowerCase();
21023
+ if (typeof value === "string") {
21024
+ actionLookup.set(lower, value);
21025
+ taskLookup.set(lower, value);
21026
+ } else {
21027
+ actionLookup.set(lower, value.default);
21028
+ taskLookup.set(lower, value.default);
21029
+ if (value.inSprint) {
21030
+ actionLookup.set(lower, `${value.default} / ${value.inSprint} (inSprint)`);
21031
+ taskLookup.set(lower, `${value.default} / ${value.inSprint} (inSprint)`);
21032
+ }
21033
+ }
21034
+ }
21035
+ } else {
21036
+ const actionMap = statusMap.legacy?.action ?? DEFAULT_ACTION_STATUS_MAP;
21037
+ const taskMap = statusMap.legacy?.task ?? DEFAULT_TASK_STATUS_MAP;
21038
+ for (const [marvin, jiraStatuses] of Object.entries(actionMap)) {
21039
+ for (const js of jiraStatuses) actionLookup.set(js.toLowerCase(), marvin);
21040
+ }
21041
+ for (const [marvin, jiraStatuses] of Object.entries(taskMap)) {
21042
+ for (const js of jiraStatuses) taskLookup.set(js.toLowerCase(), marvin);
21043
+ }
20954
21044
  }
20955
21045
  const parts = [
20956
21046
  `Found ${statusCounts.size} distinct statuses in ${resolvedProjectKey} (scanned ${data.issues.length} of ${data.total} issues):`,
@@ -20971,25 +21061,20 @@ function createJiraTools(store, projectConfig) {
20971
21061
  if (!taskTarget) unmappedTask.push(status);
20972
21062
  }
20973
21063
  if (unmappedAction.length > 0 || unmappedTask.length > 0) {
21064
+ const allUnmapped = [.../* @__PURE__ */ new Set([...unmappedAction, ...unmappedTask])];
20974
21065
  parts.push("");
20975
21066
  parts.push("To fix unmapped statuses, add jira.statusMap to .marvin/config.yaml:");
20976
21067
  parts.push(" jira:");
20977
21068
  parts.push(" statusMap:");
20978
- if (unmappedAction.length > 0) {
20979
- parts.push(" action:");
20980
- parts.push(` # Map these: ${unmappedAction.join(", ")}`);
20981
- parts.push(" # <marvin-status>: [<jira-status>, ...]");
20982
- }
20983
- if (unmappedTask.length > 0) {
20984
- parts.push(" task:");
20985
- parts.push(` # Map these: ${unmappedTask.join(", ")}`);
20986
- parts.push(" # <marvin-status>: [<jira-status>, ...]");
21069
+ for (const s of allUnmapped) {
21070
+ parts.push(` "${s}": <marvin-status>`);
20987
21071
  }
21072
+ parts.push(" # Supported marvin statuses: done, in-progress, review, ready, blocked, backlog, open");
20988
21073
  } else {
20989
21074
  parts.push("");
20990
21075
  parts.push("All statuses are mapped.");
20991
21076
  }
20992
- const usingConfig = statusMap?.action || statusMap?.task;
21077
+ const usingConfig = statusMap.flat || statusMap.legacy;
20993
21078
  parts.push("");
20994
21079
  parts.push(usingConfig ? "Using status maps from .marvin/config.yaml." : "Using built-in default status maps (no jira.statusMap in config).");
20995
21080
  return {