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.
package/dist/marvin.js CHANGED
@@ -14497,11 +14497,25 @@ function evaluateHealth(projectName, metrics) {
14497
14497
 
14498
14498
  // src/storage/progress.ts
14499
14499
  var DONE_STATUSES = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
14500
+ var STATUS_PROGRESS_DEFAULTS = {
14501
+ done: 100,
14502
+ closed: 100,
14503
+ resolved: 100,
14504
+ obsolete: 100,
14505
+ "wont do": 100,
14506
+ cancelled: 100,
14507
+ review: 80,
14508
+ "in-progress": 40,
14509
+ ready: 5,
14510
+ blocked: 10,
14511
+ backlog: 0,
14512
+ open: 0
14513
+ };
14500
14514
  function getEffectiveProgress(frontmatter) {
14501
14515
  if (DONE_STATUSES.has(frontmatter.status)) return 100;
14502
14516
  const raw = frontmatter.progress;
14503
14517
  if (typeof raw === "number") return Math.max(0, Math.min(100, Math.round(raw)));
14504
- return 0;
14518
+ return STATUS_PROGRESS_DEFAULTS[frontmatter.status] ?? 0;
14505
14519
  }
14506
14520
  function propagateProgressFromTask(store, taskId) {
14507
14521
  const updated = [];
@@ -25338,24 +25352,80 @@ var DEFAULT_TASK_STATUS_MAP = {
25338
25352
  blocked: ["Blocked"],
25339
25353
  backlog: ["To Do", "Open", "Backlog", "New"]
25340
25354
  };
25341
- function buildStatusLookup(configMap, defaults) {
25342
- const map2 = configMap ?? defaults;
25355
+ function isLegacyFormat(statusMap) {
25356
+ if (!statusMap || typeof statusMap !== "object") return false;
25357
+ const keys = Object.keys(statusMap);
25358
+ if (!keys.every((k) => k === "action" || k === "task")) return false;
25359
+ for (const key of keys) {
25360
+ const val = statusMap[key];
25361
+ if (typeof val !== "object" || val === null) return false;
25362
+ for (const innerVal of Object.values(val)) {
25363
+ if (!Array.isArray(innerVal)) return false;
25364
+ if (!innerVal.every((v) => typeof v === "string")) return false;
25365
+ }
25366
+ }
25367
+ return true;
25368
+ }
25369
+ function buildLegacyLookup(legacyMap) {
25343
25370
  const lookup = /* @__PURE__ */ new Map();
25344
- for (const [marvinStatus, jiraStatuses] of Object.entries(map2)) {
25371
+ for (const [marvinStatus, jiraStatuses] of Object.entries(legacyMap)) {
25345
25372
  for (const js of jiraStatuses) {
25346
25373
  lookup.set(js.toLowerCase(), marvinStatus);
25347
25374
  }
25348
25375
  }
25349
25376
  return lookup;
25350
25377
  }
25351
- function mapJiraStatusForAction(status, configMap) {
25352
- const lookup = buildStatusLookup(configMap, DEFAULT_ACTION_STATUS_MAP);
25378
+ function buildFlatLookup(flatMap, inSprint) {
25379
+ const lookup = /* @__PURE__ */ new Map();
25380
+ for (const [jiraStatus, value] of Object.entries(flatMap)) {
25381
+ if (typeof value === "string") {
25382
+ lookup.set(jiraStatus.toLowerCase(), value);
25383
+ } else {
25384
+ const resolved = inSprint && value.inSprint ? value.inSprint : value.default;
25385
+ lookup.set(jiraStatus.toLowerCase(), resolved);
25386
+ }
25387
+ }
25388
+ return lookup;
25389
+ }
25390
+ function normalizeStatusMap(statusMap) {
25391
+ if (!statusMap) return {};
25392
+ if (isLegacyFormat(statusMap)) {
25393
+ return { legacy: statusMap };
25394
+ }
25395
+ return { flat: statusMap };
25396
+ }
25397
+ function mapJiraStatusForAction(status, resolved, inSprint = false) {
25398
+ if (resolved.flat) {
25399
+ const lookup2 = buildFlatLookup(resolved.flat, inSprint);
25400
+ return lookup2.get(status.toLowerCase()) ?? "open";
25401
+ }
25402
+ const lookup = buildLegacyLookup(resolved.legacy?.action ?? DEFAULT_ACTION_STATUS_MAP);
25353
25403
  return lookup.get(status.toLowerCase()) ?? "open";
25354
25404
  }
25355
- function mapJiraStatusForTask(status, configMap) {
25356
- const lookup = buildStatusLookup(configMap, DEFAULT_TASK_STATUS_MAP);
25405
+ function mapJiraStatusForTask(status, resolved, inSprint = false) {
25406
+ if (resolved.flat) {
25407
+ const lookup2 = buildFlatLookup(resolved.flat, inSprint);
25408
+ return lookup2.get(status.toLowerCase()) ?? "backlog";
25409
+ }
25410
+ const lookup = buildLegacyLookup(resolved.legacy?.task ?? DEFAULT_TASK_STATUS_MAP);
25357
25411
  return lookup.get(status.toLowerCase()) ?? "backlog";
25358
25412
  }
25413
+ function isInActiveSprint(store, tags) {
25414
+ if (!tags) return false;
25415
+ const sprintTags = tags.filter((t) => t.startsWith("sprint:"));
25416
+ if (sprintTags.length === 0) return false;
25417
+ for (const tag of sprintTags) {
25418
+ const sprintId = tag.slice(7);
25419
+ const sprintDoc = store.get(sprintId);
25420
+ if (sprintDoc) {
25421
+ const status = sprintDoc.frontmatter.status;
25422
+ if (status === "active" || status === "completed") {
25423
+ return true;
25424
+ }
25425
+ }
25426
+ }
25427
+ return false;
25428
+ }
25359
25429
  function extractJiraKeyFromTags(tags) {
25360
25430
  if (!tags) return void 0;
25361
25431
  const tag = tags.find((t) => /^jira:[A-Z]+-\d+$/i.test(t));
@@ -25397,7 +25467,9 @@ async function fetchJiraStatus(store, client, host, artifactId, statusMap) {
25397
25467
  const artifactType = doc.frontmatter.type;
25398
25468
  try {
25399
25469
  const issue2 = await client.getIssueWithLinks(jiraKey);
25400
- const proposedStatus = artifactType === "task" ? mapJiraStatusForTask(issue2.fields.status.name, statusMap?.task) : mapJiraStatusForAction(issue2.fields.status.name, statusMap?.action);
25470
+ const inSprint = isInActiveSprint(store, doc.frontmatter.tags);
25471
+ const resolved = statusMap ?? {};
25472
+ const proposedStatus = artifactType === "task" ? mapJiraStatusForTask(issue2.fields.status.name, resolved, inSprint) : mapJiraStatusForAction(issue2.fields.status.name, resolved, inSprint);
25401
25473
  const currentStatus = doc.frontmatter.status;
25402
25474
  const linkedIssues = [];
25403
25475
  if (issue2.fields.subtasks) {
@@ -25749,7 +25821,7 @@ async function fetchJiraDaily(store, client, host, projectKey, dateRange, status
25749
25821
  const batch = issues.slice(i, i + BATCH_SIZE2);
25750
25822
  const results = await Promise.allSettled(
25751
25823
  batch.map(
25752
- (issue2) => processIssue(issue2, client, host, dateRange, jiraKeyToArtifacts, allDocs, statusMap)
25824
+ (issue2) => processIssue(issue2, client, host, dateRange, jiraKeyToArtifacts, allDocs, statusMap, store)
25753
25825
  )
25754
25826
  );
25755
25827
  for (let j = 0; j < results.length; j++) {
@@ -25766,7 +25838,7 @@ async function fetchJiraDaily(store, client, host, projectKey, dateRange, status
25766
25838
  summary.proposedActions = generateProposedActions(summary.issues);
25767
25839
  return summary;
25768
25840
  }
25769
- async function processIssue(issue2, client, host, dateRange, jiraKeyToArtifacts, allDocs, statusMap) {
25841
+ async function processIssue(issue2, client, host, dateRange, jiraKeyToArtifacts, allDocs, statusMap, store) {
25770
25842
  const [changelogResult, commentsResult, remoteLinksResult, issueWithLinks] = await Promise.all([
25771
25843
  client.getChangelog(issue2.key).catch(() => []),
25772
25844
  client.getComments(issue2.key).catch(() => []),
@@ -25854,7 +25926,9 @@ async function processIssue(issue2, client, host, dateRange, jiraKeyToArtifacts,
25854
25926
  if (artifactType === "action" || artifactType === "task") {
25855
25927
  const jiraStatus = issue2.fields.status?.name;
25856
25928
  if (jiraStatus) {
25857
- proposedStatus = artifactType === "task" ? mapJiraStatusForTask(jiraStatus, statusMap?.task) : mapJiraStatusForAction(jiraStatus, statusMap?.action);
25929
+ const inSprint = store ? isInActiveSprint(store, fm.tags) : false;
25930
+ const resolved = statusMap ?? {};
25931
+ proposedStatus = artifactType === "task" ? mapJiraStatusForTask(jiraStatus, resolved, inSprint) : mapJiraStatusForAction(jiraStatus, resolved, inSprint);
25858
25932
  }
25859
25933
  }
25860
25934
  marvinArtifacts.push({
@@ -25983,20 +26057,6 @@ var COMPLEXITY_WEIGHTS = {
25983
26057
  "very-complex": 8
25984
26058
  };
25985
26059
  var DEFAULT_WEIGHT = 3;
25986
- var STATUS_PROGRESS_DEFAULTS = {
25987
- done: 100,
25988
- closed: 100,
25989
- resolved: 100,
25990
- obsolete: 100,
25991
- "wont do": 100,
25992
- cancelled: 100,
25993
- review: 80,
25994
- "in-progress": 40,
25995
- ready: 5,
25996
- backlog: 0,
25997
- open: 0
25998
- };
25999
- var BLOCKED_DEFAULT_PROGRESS = 10;
26000
26060
  function resolveWeight(complexity) {
26001
26061
  if (complexity && complexity in COMPLEXITY_WEIGHTS) {
26002
26062
  return { weight: COMPLEXITY_WEIGHTS[complexity], weightSource: "complexity" };
@@ -26012,9 +26072,6 @@ function resolveProgress(frontmatter, commentAnalysisProgress) {
26012
26072
  return { progress: Math.max(0, Math.min(100, Math.round(commentAnalysisProgress))), progressSource: "comment-analysis" };
26013
26073
  }
26014
26074
  const status = frontmatter.status;
26015
- if (status === "blocked") {
26016
- return { progress: BLOCKED_DEFAULT_PROGRESS, progressSource: "status-default" };
26017
- }
26018
26075
  const defaultProgress = STATUS_PROGRESS_DEFAULTS[status] ?? 0;
26019
26076
  return { progress: defaultProgress, progressSource: "status-default" };
26020
26077
  }
@@ -26106,7 +26163,9 @@ async function assessSprintProgress(store, client, host, options = {}) {
26106
26163
  const commentSignals = [];
26107
26164
  if (jiraData) {
26108
26165
  jiraStatus = jiraData.issue.fields.status.name;
26109
- proposedMarvinStatus = fm.type === "task" ? mapJiraStatusForTask(jiraStatus, options.statusMap?.task) : mapJiraStatusForAction(jiraStatus, options.statusMap?.action);
26166
+ const inSprint = isInActiveSprint(store, fm.tags);
26167
+ const resolved = options.statusMap ?? {};
26168
+ proposedMarvinStatus = fm.type === "task" ? mapJiraStatusForTask(jiraStatus, resolved, inSprint) : mapJiraStatusForAction(jiraStatus, resolved, inSprint);
26110
26169
  const subtasks = jiraData.issue.fields.subtasks ?? [];
26111
26170
  if (subtasks.length > 0) {
26112
26171
  jiraSubtaskProgress = computeSubtaskProgress(subtasks);
@@ -26137,6 +26196,20 @@ async function assessSprintProgress(store, client, host, options = {}) {
26137
26196
  proposedValue: jiraSubtaskProgress,
26138
26197
  reason: `Jira ${jiraKey} subtask progress is ${jiraSubtaskProgress}%`
26139
26198
  });
26199
+ } else if (statusDrift && proposedMarvinStatus && !fm.progressOverride) {
26200
+ const hasExplicitProgress = "progress" in fm && typeof fm.progress === "number";
26201
+ if (!hasExplicitProgress) {
26202
+ const proposedProgress = STATUS_PROGRESS_DEFAULTS[proposedMarvinStatus] ?? 0;
26203
+ if (proposedProgress !== currentProgress) {
26204
+ proposedUpdates.push({
26205
+ artifactId: fm.id,
26206
+ field: "progress",
26207
+ currentValue: currentProgress,
26208
+ proposedValue: proposedProgress,
26209
+ reason: `Status changing to "${proposedMarvinStatus}" \u2192 default progress ${proposedProgress}%`
26210
+ });
26211
+ }
26212
+ }
26140
26213
  }
26141
26214
  const tags = fm.tags ?? [];
26142
26215
  const focusTag = tags.find((t) => t.startsWith("focus:"));
@@ -26515,7 +26588,7 @@ function findByJiraKey(store, jiraKey) {
26515
26588
  function createJiraTools(store, projectConfig) {
26516
26589
  const jiraUserConfig = loadUserConfig().jira;
26517
26590
  const defaultProjectKey = projectConfig?.jira?.projectKey;
26518
- const statusMap = projectConfig?.jira?.statusMap;
26591
+ const statusMap = normalizeStatusMap(projectConfig?.jira?.statusMap);
26519
26592
  return [
26520
26593
  // --- Local read tools ---
26521
26594
  tool20(
@@ -27142,15 +27215,32 @@ function createJiraTools(store, projectConfig) {
27142
27215
  const s = issue2.fields.status.name;
27143
27216
  statusCounts.set(s, (statusCounts.get(s) ?? 0) + 1);
27144
27217
  }
27145
- const actionMap = statusMap?.action ?? DEFAULT_ACTION_STATUS_MAP;
27146
- const taskMap = statusMap?.task ?? DEFAULT_TASK_STATUS_MAP;
27147
27218
  const actionLookup = /* @__PURE__ */ new Map();
27148
- for (const [marvin, jiraStatuses] of Object.entries(actionMap)) {
27149
- for (const js of jiraStatuses) actionLookup.set(js.toLowerCase(), marvin);
27150
- }
27151
27219
  const taskLookup = /* @__PURE__ */ new Map();
27152
- for (const [marvin, jiraStatuses] of Object.entries(taskMap)) {
27153
- for (const js of jiraStatuses) taskLookup.set(js.toLowerCase(), marvin);
27220
+ if (statusMap.flat) {
27221
+ for (const [jiraStatus, value] of Object.entries(statusMap.flat)) {
27222
+ const lower = jiraStatus.toLowerCase();
27223
+ if (typeof value === "string") {
27224
+ actionLookup.set(lower, value);
27225
+ taskLookup.set(lower, value);
27226
+ } else {
27227
+ actionLookup.set(lower, value.default);
27228
+ taskLookup.set(lower, value.default);
27229
+ if (value.inSprint) {
27230
+ actionLookup.set(lower, `${value.default} / ${value.inSprint} (inSprint)`);
27231
+ taskLookup.set(lower, `${value.default} / ${value.inSprint} (inSprint)`);
27232
+ }
27233
+ }
27234
+ }
27235
+ } else {
27236
+ const actionMap = statusMap.legacy?.action ?? DEFAULT_ACTION_STATUS_MAP;
27237
+ const taskMap = statusMap.legacy?.task ?? DEFAULT_TASK_STATUS_MAP;
27238
+ for (const [marvin, jiraStatuses] of Object.entries(actionMap)) {
27239
+ for (const js of jiraStatuses) actionLookup.set(js.toLowerCase(), marvin);
27240
+ }
27241
+ for (const [marvin, jiraStatuses] of Object.entries(taskMap)) {
27242
+ for (const js of jiraStatuses) taskLookup.set(js.toLowerCase(), marvin);
27243
+ }
27154
27244
  }
27155
27245
  const parts = [
27156
27246
  `Found ${statusCounts.size} distinct statuses in ${resolvedProjectKey} (scanned ${data.issues.length} of ${data.total} issues):`,
@@ -27171,25 +27261,20 @@ function createJiraTools(store, projectConfig) {
27171
27261
  if (!taskTarget) unmappedTask.push(status);
27172
27262
  }
27173
27263
  if (unmappedAction.length > 0 || unmappedTask.length > 0) {
27264
+ const allUnmapped = [.../* @__PURE__ */ new Set([...unmappedAction, ...unmappedTask])];
27174
27265
  parts.push("");
27175
27266
  parts.push("To fix unmapped statuses, add jira.statusMap to .marvin/config.yaml:");
27176
27267
  parts.push(" jira:");
27177
27268
  parts.push(" statusMap:");
27178
- if (unmappedAction.length > 0) {
27179
- parts.push(" action:");
27180
- parts.push(` # Map these: ${unmappedAction.join(", ")}`);
27181
- parts.push(" # <marvin-status>: [<jira-status>, ...]");
27182
- }
27183
- if (unmappedTask.length > 0) {
27184
- parts.push(" task:");
27185
- parts.push(` # Map these: ${unmappedTask.join(", ")}`);
27186
- parts.push(" # <marvin-status>: [<jira-status>, ...]");
27269
+ for (const s of allUnmapped) {
27270
+ parts.push(` "${s}": <marvin-status>`);
27187
27271
  }
27272
+ parts.push(" # Supported marvin statuses: done, in-progress, review, ready, blocked, backlog, open");
27188
27273
  } else {
27189
27274
  parts.push("");
27190
27275
  parts.push("All statuses are mapped.");
27191
27276
  }
27192
- const usingConfig = statusMap?.action || statusMap?.task;
27277
+ const usingConfig = statusMap.flat || statusMap.legacy;
27193
27278
  parts.push("");
27194
27279
  parts.push(usingConfig ? "Using status maps from .marvin/config.yaml." : "Using built-in default status maps (no jira.statusMap in config).");
27195
27280
  return {
@@ -32324,7 +32409,7 @@ async function jiraSyncCommand(artifactId, options = {}) {
32324
32409
  );
32325
32410
  return;
32326
32411
  }
32327
- const statusMap = project.config.jira?.statusMap;
32412
+ const statusMap = normalizeStatusMap(project.config.jira?.statusMap);
32328
32413
  const label = artifactId ? `Checking ${artifactId} against Jira...` : "Checking all Jira-linked actions/tasks...";
32329
32414
  console.log(chalk20.dim(label));
32330
32415
  if (options.dryRun) {
@@ -32441,9 +32526,7 @@ async function jiraStatusesCommand(projectKey) {
32441
32526
  return;
32442
32527
  }
32443
32528
  console.log(chalk20.dim(`Fetching statuses from Jira project ${resolvedProjectKey}...`));
32444
- const statusMap = project.config.jira?.statusMap;
32445
- const actionMap = statusMap?.action ?? DEFAULT_ACTION_STATUS_MAP;
32446
- const taskMap = statusMap?.task ?? DEFAULT_TASK_STATUS_MAP;
32529
+ const statusMap = normalizeStatusMap(project.config.jira?.statusMap);
32447
32530
  const email3 = jiraUserConfig?.email ?? process.env.JIRA_EMAIL;
32448
32531
  const apiToken = jiraUserConfig?.apiToken ?? process.env.JIRA_API_TOKEN;
32449
32532
  const auth = "Basic " + Buffer.from(`${email3}:${apiToken}`).toString("base64");
@@ -32467,12 +32550,28 @@ async function jiraStatusesCommand(projectKey) {
32467
32550
  statusCounts.set(s, (statusCounts.get(s) ?? 0) + 1);
32468
32551
  }
32469
32552
  const actionLookup = /* @__PURE__ */ new Map();
32470
- for (const [marvin, jiraStatuses] of Object.entries(actionMap)) {
32471
- for (const js of jiraStatuses) actionLookup.set(js.toLowerCase(), marvin);
32472
- }
32473
32553
  const taskLookup = /* @__PURE__ */ new Map();
32474
- for (const [marvin, jiraStatuses] of Object.entries(taskMap)) {
32475
- for (const js of jiraStatuses) taskLookup.set(js.toLowerCase(), marvin);
32554
+ if (statusMap.flat) {
32555
+ for (const [jiraStatus, value] of Object.entries(statusMap.flat)) {
32556
+ const lower = jiraStatus.toLowerCase();
32557
+ if (typeof value === "string") {
32558
+ actionLookup.set(lower, value);
32559
+ taskLookup.set(lower, value);
32560
+ } else {
32561
+ const label = value.inSprint ? `${value.default} / ${value.inSprint} (inSprint)` : value.default;
32562
+ actionLookup.set(lower, label);
32563
+ taskLookup.set(lower, label);
32564
+ }
32565
+ }
32566
+ } else {
32567
+ const actionMap = statusMap.legacy?.action ?? DEFAULT_ACTION_STATUS_MAP;
32568
+ const taskMap = statusMap.legacy?.task ?? DEFAULT_TASK_STATUS_MAP;
32569
+ for (const [marvin, jiraStatuses] of Object.entries(actionMap)) {
32570
+ for (const js of jiraStatuses) actionLookup.set(js.toLowerCase(), marvin);
32571
+ }
32572
+ for (const [marvin, jiraStatuses] of Object.entries(taskMap)) {
32573
+ for (const js of jiraStatuses) taskLookup.set(js.toLowerCase(), marvin);
32574
+ }
32476
32575
  }
32477
32576
  console.log(
32478
32577
  `
@@ -32495,14 +32594,11 @@ Found ${chalk20.bold(String(statusCounts.size))} distinct statuses in ${chalk20.
32495
32594
  console.log(chalk20.yellow("\nSome statuses are unmapped. Add jira.statusMap to .marvin/config.yaml:"));
32496
32595
  console.log(chalk20.dim(" jira:"));
32497
32596
  console.log(chalk20.dim(" statusMap:"));
32498
- console.log(chalk20.dim(" action:"));
32499
- console.log(chalk20.dim(' <marvin-status>: ["<Jira Status>", ...]'));
32500
- console.log(chalk20.dim(" task:"));
32501
- console.log(chalk20.dim(' <marvin-status>: ["<Jira Status>", ...]'));
32597
+ console.log(chalk20.dim(' "<Jira Status>": <marvin-status>'));
32502
32598
  } else {
32503
32599
  console.log(chalk20.green("\nAll statuses are mapped."));
32504
32600
  }
32505
- const usingConfig = statusMap?.action || statusMap?.task;
32601
+ const usingConfig = statusMap.flat || statusMap.legacy;
32506
32602
  console.log(
32507
32603
  chalk20.dim(
32508
32604
  usingConfig ? "\nUsing status maps from .marvin/config.yaml." : "\nUsing built-in default status maps (no jira.statusMap in config)."
@@ -32537,7 +32633,7 @@ async function jiraDailyCommand(options) {
32537
32633
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
32538
32634
  const fromDate = options.from ?? today;
32539
32635
  const toDate = options.to ?? fromDate;
32540
- const statusMap = proj.config.jira?.statusMap;
32636
+ const statusMap = normalizeStatusMap(proj.config.jira?.statusMap);
32541
32637
  const rangeLabel = fromDate === toDate ? fromDate : `${fromDate} to ${toDate}`;
32542
32638
  console.log(
32543
32639
  chalk20.dim(`Fetching Jira daily summary for ${resolvedProjectKey} \u2014 ${rangeLabel}...`)
@@ -32654,7 +32750,7 @@ function createProgram() {
32654
32750
  const program2 = new Command();
32655
32751
  program2.name("marvin").description(
32656
32752
  "AI-powered product development assistant with Product Owner, Delivery Manager, and Technical Lead personas"
32657
- ).version("0.5.15");
32753
+ ).version("0.5.17");
32658
32754
  program2.command("init").description("Initialize a new Marvin project in the current directory").action(async () => {
32659
32755
  await initCommand();
32660
32756
  });