mrvn-cli 0.5.16 → 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,35 +19192,62 @@ var DEFAULT_TASK_STATUS_MAP = {
19178
19192
  blocked: ["Blocked"],
19179
19193
  backlog: ["To Do", "Open", "Backlog", "New"]
19180
19194
  };
19181
- function isConditionalEntry(value) {
19182
- return !Array.isArray(value) && typeof value === "object" && "default" in value;
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;
19183
19208
  }
19184
- function buildStatusLookup(configMap, defaults, inSprint = false) {
19185
- const map2 = configMap ?? defaults;
19209
+ function buildLegacyLookup(legacyMap) {
19186
19210
  const lookup = /* @__PURE__ */ new Map();
19187
- for (const [marvinStatus, value] of Object.entries(map2)) {
19188
- const statuses = isConditionalEntry(value) ? value.default : value;
19189
- for (const js of statuses) {
19211
+ for (const [marvinStatus, jiraStatuses] of Object.entries(legacyMap)) {
19212
+ for (const js of jiraStatuses) {
19190
19213
  lookup.set(js.toLowerCase(), marvinStatus);
19191
19214
  }
19192
19215
  }
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
- }
19216
+ return lookup;
19217
+ }
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);
19200
19226
  }
19201
19227
  }
19202
19228
  return lookup;
19203
19229
  }
19204
- function mapJiraStatusForAction(status, configMap, inSprint) {
19205
- const lookup = buildStatusLookup(configMap, DEFAULT_ACTION_STATUS_MAP, inSprint ?? false);
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);
19206
19243
  return lookup.get(status.toLowerCase()) ?? "open";
19207
19244
  }
19208
- function mapJiraStatusForTask(status, configMap, inSprint) {
19209
- const lookup = buildStatusLookup(configMap, DEFAULT_TASK_STATUS_MAP, inSprint ?? false);
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);
19210
19251
  return lookup.get(status.toLowerCase()) ?? "backlog";
19211
19252
  }
19212
19253
  function isInActiveSprint(store, tags) {
@@ -19267,7 +19308,8 @@ async function fetchJiraStatus(store, client, host, artifactId, statusMap) {
19267
19308
  try {
19268
19309
  const issue2 = await client.getIssueWithLinks(jiraKey);
19269
19310
  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);
19311
+ const resolved = statusMap ?? {};
19312
+ const proposedStatus = artifactType === "task" ? mapJiraStatusForTask(issue2.fields.status.name, resolved, inSprint) : mapJiraStatusForAction(issue2.fields.status.name, resolved, inSprint);
19271
19313
  const currentStatus = doc.frontmatter.status;
19272
19314
  const linkedIssues = [];
19273
19315
  if (issue2.fields.subtasks) {
@@ -19685,7 +19727,8 @@ async function processIssue(issue2, client, host, dateRange, jiraKeyToArtifacts,
19685
19727
  const jiraStatus = issue2.fields.status?.name;
19686
19728
  if (jiraStatus) {
19687
19729
  const inSprint = store ? isInActiveSprint(store, fm.tags) : false;
19688
- proposedStatus = artifactType === "task" ? mapJiraStatusForTask(jiraStatus, statusMap?.task, inSprint) : mapJiraStatusForAction(jiraStatus, statusMap?.action, inSprint);
19730
+ const resolved = statusMap ?? {};
19731
+ proposedStatus = artifactType === "task" ? mapJiraStatusForTask(jiraStatus, resolved, inSprint) : mapJiraStatusForAction(jiraStatus, resolved, inSprint);
19689
19732
  }
19690
19733
  }
19691
19734
  marvinArtifacts.push({
@@ -19814,20 +19857,6 @@ var COMPLEXITY_WEIGHTS = {
19814
19857
  "very-complex": 8
19815
19858
  };
19816
19859
  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
19860
  function resolveWeight(complexity) {
19832
19861
  if (complexity && complexity in COMPLEXITY_WEIGHTS) {
19833
19862
  return { weight: COMPLEXITY_WEIGHTS[complexity], weightSource: "complexity" };
@@ -19843,9 +19872,6 @@ function resolveProgress(frontmatter, commentAnalysisProgress) {
19843
19872
  return { progress: Math.max(0, Math.min(100, Math.round(commentAnalysisProgress))), progressSource: "comment-analysis" };
19844
19873
  }
19845
19874
  const status = frontmatter.status;
19846
- if (status === "blocked") {
19847
- return { progress: BLOCKED_DEFAULT_PROGRESS, progressSource: "status-default" };
19848
- }
19849
19875
  const defaultProgress = STATUS_PROGRESS_DEFAULTS[status] ?? 0;
19850
19876
  return { progress: defaultProgress, progressSource: "status-default" };
19851
19877
  }
@@ -19938,7 +19964,8 @@ async function assessSprintProgress(store, client, host, options = {}) {
19938
19964
  if (jiraData) {
19939
19965
  jiraStatus = jiraData.issue.fields.status.name;
19940
19966
  const inSprint = isInActiveSprint(store, fm.tags);
19941
- proposedMarvinStatus = fm.type === "task" ? mapJiraStatusForTask(jiraStatus, options.statusMap?.task, inSprint) : mapJiraStatusForAction(jiraStatus, options.statusMap?.action, inSprint);
19967
+ const resolved = options.statusMap ?? {};
19968
+ proposedMarvinStatus = fm.type === "task" ? mapJiraStatusForTask(jiraStatus, resolved, inSprint) : mapJiraStatusForAction(jiraStatus, resolved, inSprint);
19942
19969
  const subtasks = jiraData.issue.fields.subtasks ?? [];
19943
19970
  if (subtasks.length > 0) {
19944
19971
  jiraSubtaskProgress = computeSubtaskProgress(subtasks);
@@ -19969,6 +19996,20 @@ async function assessSprintProgress(store, client, host, options = {}) {
19969
19996
  proposedValue: jiraSubtaskProgress,
19970
19997
  reason: `Jira ${jiraKey} subtask progress is ${jiraSubtaskProgress}%`
19971
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
+ }
19972
20013
  }
19973
20014
  const tags = fm.tags ?? [];
19974
20015
  const focusTag = tags.find((t) => t.startsWith("focus:"));
@@ -20347,7 +20388,7 @@ function findByJiraKey(store, jiraKey) {
20347
20388
  function createJiraTools(store, projectConfig) {
20348
20389
  const jiraUserConfig = loadUserConfig().jira;
20349
20390
  const defaultProjectKey = projectConfig?.jira?.projectKey;
20350
- const statusMap = projectConfig?.jira?.statusMap;
20391
+ const statusMap = normalizeStatusMap(projectConfig?.jira?.statusMap);
20351
20392
  return [
20352
20393
  // --- Local read tools ---
20353
20394
  tool20(
@@ -20974,22 +21015,31 @@ function createJiraTools(store, projectConfig) {
20974
21015
  const s = issue2.fields.status.name;
20975
21016
  statusCounts.set(s, (statusCounts.get(s) ?? 0) + 1);
20976
21017
  }
20977
- const actionMap = statusMap?.action ?? DEFAULT_ACTION_STATUS_MAP;
20978
- const taskMap = statusMap?.task ?? DEFAULT_TASK_STATUS_MAP;
20979
21018
  const actionLookup = /* @__PURE__ */ new Map();
20980
- for (const [marvin, value] of Object.entries(actionMap)) {
20981
- const jiraStatuses = Array.isArray(value) ? value : value.default;
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
- }
20986
- }
20987
21019
  const taskLookup = /* @__PURE__ */ new Map();
20988
- for (const [marvin, value] of Object.entries(taskMap)) {
20989
- const jiraStatuses = Array.isArray(value) ? value : value.default;
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)`);
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);
20993
21043
  }
20994
21044
  }
20995
21045
  const parts = [
@@ -21011,25 +21061,20 @@ function createJiraTools(store, projectConfig) {
21011
21061
  if (!taskTarget) unmappedTask.push(status);
21012
21062
  }
21013
21063
  if (unmappedAction.length > 0 || unmappedTask.length > 0) {
21064
+ const allUnmapped = [.../* @__PURE__ */ new Set([...unmappedAction, ...unmappedTask])];
21014
21065
  parts.push("");
21015
21066
  parts.push("To fix unmapped statuses, add jira.statusMap to .marvin/config.yaml:");
21016
21067
  parts.push(" jira:");
21017
21068
  parts.push(" statusMap:");
21018
- if (unmappedAction.length > 0) {
21019
- parts.push(" action:");
21020
- parts.push(` # Map these: ${unmappedAction.join(", ")}`);
21021
- parts.push(" # <marvin-status>: [<jira-status>, ...]");
21022
- }
21023
- if (unmappedTask.length > 0) {
21024
- parts.push(" task:");
21025
- parts.push(` # Map these: ${unmappedTask.join(", ")}`);
21026
- parts.push(" # <marvin-status>: [<jira-status>, ...]");
21069
+ for (const s of allUnmapped) {
21070
+ parts.push(` "${s}": <marvin-status>`);
21027
21071
  }
21072
+ parts.push(" # Supported marvin statuses: done, in-progress, review, ready, blocked, backlog, open");
21028
21073
  } else {
21029
21074
  parts.push("");
21030
21075
  parts.push("All statuses are mapped.");
21031
21076
  }
21032
- const usingConfig = statusMap?.action || statusMap?.task;
21077
+ const usingConfig = statusMap.flat || statusMap.legacy;
21033
21078
  parts.push("");
21034
21079
  parts.push(usingConfig ? "Using status maps from .marvin/config.yaml." : "Using built-in default status maps (no jira.statusMap in config).");
21035
21080
  return {