mrvn-cli 0.3.7 → 0.4.0

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
@@ -14102,6 +14102,17 @@ function createMeetingTools(store) {
14102
14102
  // src/plugins/builtin/tools/reports.ts
14103
14103
  import { tool as tool2 } from "@anthropic-ai/claude-agent-sdk";
14104
14104
 
14105
+ // src/plugins/builtin/tools/epic-utils.ts
14106
+ function normalizeLinkedFeatures(value) {
14107
+ if (value === void 0 || value === null) return [];
14108
+ if (typeof value === "string") return [value];
14109
+ if (Array.isArray(value)) return value.filter((v) => typeof v === "string");
14110
+ return [];
14111
+ }
14112
+ function generateFeatureTags(features) {
14113
+ return features.map((id) => `feature:${id}`);
14114
+ }
14115
+
14105
14116
  // src/reports/gar/collector.ts
14106
14117
  function collectGarMetrics(store) {
14107
14118
  const allActions = store.list({ type: "action" });
@@ -14599,7 +14610,7 @@ function createReportTools(store) {
14599
14610
  id: epicDoc.frontmatter.id,
14600
14611
  title: epicDoc.frontmatter.title,
14601
14612
  status: epicDoc.frontmatter.status,
14602
- linkedFeature: epicDoc.frontmatter.linkedFeature,
14613
+ linkedFeature: normalizeLinkedFeatures(epicDoc.frontmatter.linkedFeature),
14603
14614
  targetDate: epicDoc.frontmatter.targetDate,
14604
14615
  estimatedEffort: epicDoc.frontmatter.estimatedEffort,
14605
14616
  workItems: {
@@ -14726,7 +14737,7 @@ function createReportTools(store) {
14726
14737
  const epicDocs = store.list({ type: "epic" });
14727
14738
  const features = featureDocs.filter((f) => !args.feature || f.frontmatter.id === args.feature).map((f) => {
14728
14739
  const linkedEpics = epicDocs.filter(
14729
- (e) => e.frontmatter.linkedFeature === f.frontmatter.id
14740
+ (e) => normalizeLinkedFeatures(e.frontmatter.linkedFeature).includes(f.frontmatter.id)
14730
14741
  );
14731
14742
  const byStatus = {};
14732
14743
  for (const e of linkedEpics) {
@@ -14930,14 +14941,14 @@ function createEpicTools(store) {
14930
14941
  let docs = store.list({ type: "epic", status: args.status });
14931
14942
  if (args.linkedFeature) {
14932
14943
  docs = docs.filter(
14933
- (d) => d.frontmatter.linkedFeature === args.linkedFeature
14944
+ (d) => normalizeLinkedFeatures(d.frontmatter.linkedFeature).includes(args.linkedFeature)
14934
14945
  );
14935
14946
  }
14936
14947
  const summary = docs.map((d) => ({
14937
14948
  id: d.frontmatter.id,
14938
14949
  title: d.frontmatter.title,
14939
14950
  status: d.frontmatter.status,
14940
- linkedFeature: d.frontmatter.linkedFeature,
14951
+ linkedFeature: normalizeLinkedFeatures(d.frontmatter.linkedFeature),
14941
14952
  owner: d.frontmatter.owner,
14942
14953
  targetDate: d.frontmatter.targetDate,
14943
14954
  estimatedEffort: d.frontmatter.estimatedEffort,
@@ -14978,11 +14989,11 @@ function createEpicTools(store) {
14978
14989
  ),
14979
14990
  tool4(
14980
14991
  "create_epic",
14981
- "Create a new epic linked to an approved feature. The linked feature must exist and be approved.",
14992
+ "Create a new epic linked to one or more approved features. All linked features must exist and be approved.",
14982
14993
  {
14983
14994
  title: external_exports.string().describe("Epic title"),
14984
14995
  content: external_exports.string().describe("Epic description and scope"),
14985
- linkedFeature: external_exports.string().describe("Feature ID to link this epic to (e.g. 'F-001')"),
14996
+ linkedFeature: external_exports.union([external_exports.string(), external_exports.array(external_exports.string())]).describe("Feature ID(s) to link this epic to (e.g. 'F-001' or ['F-001', 'F-002'])"),
14986
14997
  status: external_exports.enum(["planned", "in-progress", "done"]).optional().describe("Epic status (default: 'planned')"),
14987
14998
  owner: external_exports.string().optional().describe("Epic owner"),
14988
14999
  targetDate: external_exports.string().optional().describe("Target completion date (ISO format)"),
@@ -14990,45 +15001,48 @@ function createEpicTools(store) {
14990
15001
  tags: external_exports.array(external_exports.string()).optional().describe("Additional tags")
14991
15002
  },
14992
15003
  async (args) => {
14993
- const feature = store.get(args.linkedFeature);
14994
- if (!feature) {
14995
- return {
14996
- content: [
14997
- {
14998
- type: "text",
14999
- text: `Feature ${args.linkedFeature} not found`
15000
- }
15001
- ],
15002
- isError: true
15003
- };
15004
- }
15005
- if (feature.frontmatter.type !== "feature") {
15006
- return {
15007
- content: [
15008
- {
15009
- type: "text",
15010
- text: `${args.linkedFeature} is a ${feature.frontmatter.type}, not a feature`
15011
- }
15012
- ],
15013
- isError: true
15014
- };
15015
- }
15016
- if (feature.frontmatter.status !== "approved") {
15017
- return {
15018
- content: [
15019
- {
15020
- type: "text",
15021
- text: `Feature ${args.linkedFeature} has status '${feature.frontmatter.status}'. Only approved features can have epics. Ask the Product Owner to approve it first.`
15022
- }
15023
- ],
15024
- isError: true
15025
- };
15004
+ const linkedFeatures = normalizeLinkedFeatures(args.linkedFeature);
15005
+ for (const featureId of linkedFeatures) {
15006
+ const feature = store.get(featureId);
15007
+ if (!feature) {
15008
+ return {
15009
+ content: [
15010
+ {
15011
+ type: "text",
15012
+ text: `Feature ${featureId} not found`
15013
+ }
15014
+ ],
15015
+ isError: true
15016
+ };
15017
+ }
15018
+ if (feature.frontmatter.type !== "feature") {
15019
+ return {
15020
+ content: [
15021
+ {
15022
+ type: "text",
15023
+ text: `${featureId} is a ${feature.frontmatter.type}, not a feature`
15024
+ }
15025
+ ],
15026
+ isError: true
15027
+ };
15028
+ }
15029
+ if (feature.frontmatter.status !== "approved") {
15030
+ return {
15031
+ content: [
15032
+ {
15033
+ type: "text",
15034
+ text: `Feature ${featureId} has status '${feature.frontmatter.status}'. Only approved features can have epics. Ask the Product Owner to approve it first.`
15035
+ }
15036
+ ],
15037
+ isError: true
15038
+ };
15039
+ }
15026
15040
  }
15027
15041
  const frontmatter = {
15028
15042
  title: args.title,
15029
15043
  status: args.status ?? "planned",
15030
- linkedFeature: args.linkedFeature,
15031
- tags: [`feature:${args.linkedFeature}`, ...args.tags ?? []]
15044
+ linkedFeature: linkedFeatures,
15045
+ tags: [...generateFeatureTags(linkedFeatures), ...args.tags ?? []]
15032
15046
  };
15033
15047
  if (args.owner) frontmatter.owner = args.owner;
15034
15048
  if (args.targetDate) frontmatter.targetDate = args.targetDate;
@@ -15038,7 +15052,7 @@ function createEpicTools(store) {
15038
15052
  content: [
15039
15053
  {
15040
15054
  type: "text",
15041
- text: `Created epic ${doc.frontmatter.id}: ${doc.frontmatter.title} (linked to ${args.linkedFeature})`
15055
+ text: `Created epic ${doc.frontmatter.id}: ${doc.frontmatter.title} (linked to ${linkedFeatures.join(", ")})`
15042
15056
  }
15043
15057
  ]
15044
15058
  };
@@ -15046,7 +15060,7 @@ function createEpicTools(store) {
15046
15060
  ),
15047
15061
  tool4(
15048
15062
  "update_epic",
15049
- "Update an existing epic. The linked feature cannot be changed.",
15063
+ "Update an existing epic, including its linked features.",
15050
15064
  {
15051
15065
  id: external_exports.string().describe("Epic ID to update"),
15052
15066
  title: external_exports.string().optional().describe("New title"),
@@ -15055,10 +15069,49 @@ function createEpicTools(store) {
15055
15069
  owner: external_exports.string().optional().describe("New owner"),
15056
15070
  targetDate: external_exports.string().optional().describe("New target date"),
15057
15071
  estimatedEffort: external_exports.string().optional().describe("New estimated effort"),
15072
+ linkedFeature: external_exports.union([external_exports.string(), external_exports.array(external_exports.string())]).optional().describe("New linked feature ID(s)"),
15058
15073
  tags: external_exports.array(external_exports.string()).optional().describe("Replace tags (e.g. remove 'risk', add 'risk-mitigated')")
15059
15074
  },
15060
15075
  async (args) => {
15061
- const { id, content, ...updates } = args;
15076
+ const { id, content, linkedFeature: rawLinkedFeature, tags: userTags, ...updates } = args;
15077
+ if (rawLinkedFeature !== void 0) {
15078
+ const linkedFeatures = normalizeLinkedFeatures(rawLinkedFeature);
15079
+ for (const featureId of linkedFeatures) {
15080
+ const feature = store.get(featureId);
15081
+ if (!feature) {
15082
+ return {
15083
+ content: [
15084
+ { type: "text", text: `Feature ${featureId} not found` }
15085
+ ],
15086
+ isError: true
15087
+ };
15088
+ }
15089
+ if (feature.frontmatter.type !== "feature") {
15090
+ return {
15091
+ content: [
15092
+ { type: "text", text: `${featureId} is a ${feature.frontmatter.type}, not a feature` }
15093
+ ],
15094
+ isError: true
15095
+ };
15096
+ }
15097
+ if (feature.frontmatter.status !== "approved") {
15098
+ return {
15099
+ content: [
15100
+ { type: "text", text: `Feature ${featureId} has status '${feature.frontmatter.status}'. Only approved features can have epics. Ask the Product Owner to approve it first.` }
15101
+ ],
15102
+ isError: true
15103
+ };
15104
+ }
15105
+ }
15106
+ updates.linkedFeature = linkedFeatures;
15107
+ const existingDoc = store.get(id);
15108
+ const existingTags = existingDoc?.frontmatter.tags ?? [];
15109
+ const nonFeatureTags = existingTags.filter((t) => !t.startsWith("feature:"));
15110
+ const baseTags = userTags ?? nonFeatureTags;
15111
+ updates.tags = [...generateFeatureTags(linkedFeatures), ...baseTags];
15112
+ } else if (userTags !== void 0) {
15113
+ updates.tags = userTags;
15114
+ }
15062
15115
  const doc = store.update(id, updates, content);
15063
15116
  return {
15064
15117
  content: [
@@ -15393,7 +15446,9 @@ function createSprintPlanningTools(store) {
15393
15446
  const questions = store.list({ type: "question", status: "open" });
15394
15447
  const contributions = store.list({ type: "contribution" });
15395
15448
  const approvedFeatures = features.filter((f) => f.frontmatter.status === "approved").sort((a, b) => priorityRank(a.frontmatter.priority) - priorityRank(b.frontmatter.priority)).map((f) => {
15396
- const linkedEpics = epics.filter((e) => e.frontmatter.linkedFeature === f.frontmatter.id);
15449
+ const linkedEpics = epics.filter(
15450
+ (e) => normalizeLinkedFeatures(e.frontmatter.linkedFeature).includes(f.frontmatter.id)
15451
+ );
15397
15452
  const epicsByStatus = {};
15398
15453
  for (const e of linkedEpics) {
15399
15454
  epicsByStatus[e.frontmatter.status] = (epicsByStatus[e.frontmatter.status] ?? 0) + 1;
@@ -15418,22 +15473,25 @@ function createSprintPlanningTools(store) {
15418
15473
  );
15419
15474
  if (args.focusFeature) {
15420
15475
  backlogEpics = backlogEpics.filter(
15421
- (e) => e.frontmatter.linkedFeature === args.focusFeature
15476
+ (e) => normalizeLinkedFeatures(e.frontmatter.linkedFeature).includes(args.focusFeature)
15422
15477
  );
15423
15478
  }
15424
15479
  const backlog = backlogEpics.sort((a, b) => {
15425
- const fa = featureMap.get(a.frontmatter.linkedFeature);
15426
- const fb = featureMap.get(b.frontmatter.linkedFeature);
15427
- return priorityRank(fa?.frontmatter.priority) - priorityRank(fb?.frontmatter.priority);
15480
+ const aFeatures = normalizeLinkedFeatures(a.frontmatter.linkedFeature);
15481
+ const bFeatures = normalizeLinkedFeatures(b.frontmatter.linkedFeature);
15482
+ const aRank = Math.min(...aFeatures.map((id) => priorityRank(featureMap.get(id)?.frontmatter.priority)), 99);
15483
+ const bRank = Math.min(...bFeatures.map((id) => priorityRank(featureMap.get(id)?.frontmatter.priority)), 99);
15484
+ return aRank - bRank;
15428
15485
  }).map((e) => {
15429
- const parent = featureMap.get(e.frontmatter.linkedFeature);
15486
+ const linkedFeatures = normalizeLinkedFeatures(e.frontmatter.linkedFeature);
15487
+ const parents = linkedFeatures.map((id) => featureMap.get(id)).filter(Boolean);
15430
15488
  return {
15431
15489
  id: e.frontmatter.id,
15432
15490
  title: e.frontmatter.title,
15433
15491
  status: e.frontmatter.status,
15434
- linkedFeature: e.frontmatter.linkedFeature,
15435
- featureTitle: parent?.frontmatter.title ?? null,
15436
- featurePriority: parent?.frontmatter.priority ?? null,
15492
+ linkedFeature: linkedFeatures,
15493
+ featureTitle: parents.map((p) => p.frontmatter.title).join(", ") || null,
15494
+ featurePriority: parents.map((p) => p.frontmatter.priority).join(", ") || null,
15437
15495
  estimatedEffort: e.frontmatter.estimatedEffort ?? null,
15438
15496
  targetDate: e.frontmatter.targetDate ?? null
15439
15497
  };
@@ -15514,8 +15572,8 @@ function createSprintPlanningTools(store) {
15514
15572
  const epicsAtRisk = epics.filter((e) => {
15515
15573
  if (e.frontmatter.status === "done") return false;
15516
15574
  if (e.frontmatter.targetDate && e.frontmatter.targetDate < now) return true;
15517
- const parent = featureMap.get(e.frontmatter.linkedFeature);
15518
- if (parent?.frontmatter.status === "deferred") return true;
15575
+ const linkedIds = normalizeLinkedFeatures(e.frontmatter.linkedFeature);
15576
+ if (linkedIds.some((id) => featureMap.get(id)?.frontmatter.status === "deferred")) return true;
15519
15577
  return false;
15520
15578
  }).map((e) => ({
15521
15579
  id: e.frontmatter.id,
@@ -18049,7 +18107,7 @@ function getDiagramData(store) {
18049
18107
  id: fm.id,
18050
18108
  title: fm.title,
18051
18109
  status: fm.status,
18052
- linkedFeature: fm.linkedFeature
18110
+ linkedFeature: normalizeLinkedFeatures(fm.linkedFeature)
18053
18111
  });
18054
18112
  break;
18055
18113
  case "feature":
@@ -18840,8 +18898,8 @@ function buildArtifactFlowchart(data) {
18840
18898
  lines.push(" classDef default fill:#1e293b,stroke:#475569,color:#e2e8f0");
18841
18899
  const nodeIds = /* @__PURE__ */ new Set();
18842
18900
  for (const epic of data.epics) {
18843
- if (epic.linkedFeature) {
18844
- const feature = data.features.find((f) => f.id === epic.linkedFeature);
18901
+ for (const featureId of epic.linkedFeature) {
18902
+ const feature = data.features.find((f) => f.id === featureId);
18845
18903
  if (feature) {
18846
18904
  const fNode = feature.id.replace(/-/g, "_");
18847
18905
  const eNode = epic.id.replace(/-/g, "_");
@@ -23644,7 +23702,7 @@ function createProgram() {
23644
23702
  const program2 = new Command();
23645
23703
  program2.name("marvin").description(
23646
23704
  "AI-powered product development assistant with Product Owner, Delivery Manager, and Technical Lead personas"
23647
- ).version("0.3.7");
23705
+ ).version("0.4.0");
23648
23706
  program2.command("init").description("Initialize a new Marvin project in the current directory").action(async () => {
23649
23707
  await initCommand();
23650
23708
  });