mrvn-cli 0.4.7 → 0.4.9

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/README.md CHANGED
@@ -751,4 +751,10 @@ npm run typecheck # TypeScript check without emitting
751
751
 
752
752
  ## License
753
753
 
754
- MIT
754
+ Marvin CLI is open-source and released under the MIT License with Commons Clause. This means you can:
755
+
756
+ - Use Marvin for personal and commercial projects
757
+ - Modify the source code to fit your needs
758
+ - Distribute copies of the software
759
+ - Contribute improvements back to the community
760
+ - The Commons Clause adds one important restriction: you cannot sell Marvin itself as a service or product, but you can use it in your own applications and services.
package/dist/index.js CHANGED
@@ -14557,7 +14557,8 @@ function createActionTools(store) {
14557
14557
  priority: external_exports.string().optional().describe("Priority (high, medium, low)"),
14558
14558
  tags: external_exports.array(external_exports.string()).optional().describe("Tags for categorization"),
14559
14559
  dueDate: external_exports.string().optional().describe("Due date in ISO format (e.g. '2026-03-15')"),
14560
- sprints: external_exports.array(external_exports.string()).optional().describe("Sprint IDs to assign (e.g. ['SP-001']). Adds sprint:SP-xxx tags.")
14560
+ sprints: external_exports.array(external_exports.string()).optional().describe("Sprint IDs to assign (e.g. ['SP-001']). Adds sprint:SP-xxx tags."),
14561
+ workStream: external_exports.string().optional().describe("Work stream name (e.g. 'Budget UX'). Adds a stream:<value> tag.")
14561
14562
  },
14562
14563
  async (args) => {
14563
14564
  const tags = [...args.tags ?? []];
@@ -14567,6 +14568,9 @@ function createActionTools(store) {
14567
14568
  if (!tags.includes(tag)) tags.push(tag);
14568
14569
  }
14569
14570
  }
14571
+ if (args.workStream) {
14572
+ tags.push(`stream:${args.workStream}`);
14573
+ }
14570
14574
  const doc = store.create(
14571
14575
  "action",
14572
14576
  {
@@ -14604,10 +14608,11 @@ function createActionTools(store) {
14604
14608
  priority: external_exports.string().optional().describe("New priority"),
14605
14609
  dueDate: external_exports.string().optional().describe("Due date in ISO format (e.g. '2026-03-15')"),
14606
14610
  tags: external_exports.array(external_exports.string()).optional().describe("Replace all tags. When provided with sprints, sprint tags are merged into this array."),
14607
- sprints: external_exports.array(external_exports.string()).optional().describe("Sprint IDs to assign (replaces existing sprint tags). E.g. ['SP-001'].")
14611
+ sprints: external_exports.array(external_exports.string()).optional().describe("Sprint IDs to assign (replaces existing sprint tags). E.g. ['SP-001']."),
14612
+ workStream: external_exports.string().optional().describe("Work stream name (e.g. 'Budget UX'). Replaces existing stream:<value> tag.")
14608
14613
  },
14609
14614
  async (args) => {
14610
- const { id, content, sprints, tags, ...updates } = args;
14615
+ const { id, content, sprints, tags, workStream, ...updates } = args;
14611
14616
  if (tags !== void 0) {
14612
14617
  const merged = [...tags];
14613
14618
  if (sprints) {
@@ -14616,8 +14621,14 @@ function createActionTools(store) {
14616
14621
  if (!merged.includes(tag)) merged.push(tag);
14617
14622
  }
14618
14623
  }
14619
- updates.tags = merged;
14620
- } else if (sprints !== void 0) {
14624
+ if (workStream !== void 0) {
14625
+ const filtered = merged.filter((t) => !t.startsWith("stream:"));
14626
+ filtered.push(`stream:${workStream}`);
14627
+ updates.tags = filtered;
14628
+ } else {
14629
+ updates.tags = merged;
14630
+ }
14631
+ } else if (sprints !== void 0 || workStream !== void 0) {
14621
14632
  const existing = store.get(id);
14622
14633
  if (!existing) {
14623
14634
  return {
@@ -14625,10 +14636,16 @@ function createActionTools(store) {
14625
14636
  isError: true
14626
14637
  };
14627
14638
  }
14628
- const existingTags = existing.frontmatter.tags ?? [];
14629
- const nonSprintTags = existingTags.filter((t) => !t.startsWith("sprint:"));
14630
- const newSprintTags = sprints.map((s) => `sprint:${s}`);
14631
- updates.tags = [...nonSprintTags, ...newSprintTags];
14639
+ let existingTags = existing.frontmatter.tags ?? [];
14640
+ if (sprints !== void 0) {
14641
+ existingTags = existingTags.filter((t) => !t.startsWith("sprint:"));
14642
+ existingTags.push(...sprints.map((s) => `sprint:${s}`));
14643
+ }
14644
+ if (workStream !== void 0) {
14645
+ existingTags = existingTags.filter((t) => !t.startsWith("stream:"));
14646
+ existingTags.push(`stream:${workStream}`);
14647
+ }
14648
+ updates.tags = existingTags;
14632
14649
  }
14633
14650
  const doc = store.update(id, updates, content);
14634
14651
  return {
@@ -14793,14 +14810,19 @@ function createDocumentTools(store) {
14793
14810
  {
14794
14811
  type: external_exports.string().optional().describe(`Filter by document type (registered types: ${store.registeredTypes.join(", ")})`),
14795
14812
  status: external_exports.string().optional().describe("Filter by status"),
14796
- tag: external_exports.string().optional().describe("Filter by tag")
14813
+ tag: external_exports.string().optional().describe("Filter by tag"),
14814
+ workStream: external_exports.string().optional().describe("Filter by work stream name (matches stream:<value> tag)")
14797
14815
  },
14798
14816
  async (args) => {
14799
- const docs = store.list({
14817
+ let docs = store.list({
14800
14818
  type: args.type,
14801
14819
  status: args.status,
14802
14820
  tag: args.tag
14803
14821
  });
14822
+ if (args.workStream) {
14823
+ const streamTag = `stream:${args.workStream}`;
14824
+ docs = docs.filter((d) => d.frontmatter.tags?.includes(streamTag));
14825
+ }
14804
14826
  const summary = docs.map((d) => ({
14805
14827
  id: d.frontmatter.id,
14806
14828
  title: d.frontmatter.title,
@@ -15472,13 +15494,14 @@ function collectSprintSummaryData(store, sprintId) {
15472
15494
  const workItemDocs = allDocs.filter(
15473
15495
  (d) => d.frontmatter.type !== "sprint" && d.frontmatter.type !== "epic" && d.frontmatter.tags?.includes(sprintTag)
15474
15496
  );
15497
+ const primaryDocs = workItemDocs.filter((d) => d.frontmatter.type !== "contribution");
15475
15498
  const byStatus = {};
15476
15499
  const byType = {};
15477
15500
  let doneCount = 0;
15478
15501
  let inProgressCount = 0;
15479
15502
  let openCount = 0;
15480
15503
  let blockedCount = 0;
15481
- for (const doc of workItemDocs) {
15504
+ for (const doc of primaryDocs) {
15482
15505
  const s = doc.frontmatter.status;
15483
15506
  byStatus[s] = (byStatus[s] ?? 0) + 1;
15484
15507
  byType[doc.frontmatter.type] = (byType[doc.frontmatter.type] ?? 0) + 1;
@@ -15487,21 +15510,61 @@ function collectSprintSummaryData(store, sprintId) {
15487
15510
  else if (s === "blocked") blockedCount++;
15488
15511
  else openCount++;
15489
15512
  }
15513
+ const allItemsById = /* @__PURE__ */ new Map();
15514
+ const childrenByParent = /* @__PURE__ */ new Map();
15515
+ const sprintItemIds = new Set(workItemDocs.map((d) => d.frontmatter.id));
15516
+ for (const doc of workItemDocs) {
15517
+ const about = doc.frontmatter.aboutArtifact;
15518
+ const streamTag = (doc.frontmatter.tags ?? []).find((t) => t.startsWith("stream:"));
15519
+ const item = {
15520
+ id: doc.frontmatter.id,
15521
+ title: doc.frontmatter.title,
15522
+ type: doc.frontmatter.type,
15523
+ status: doc.frontmatter.status,
15524
+ workStream: streamTag ? streamTag.slice(7) : void 0,
15525
+ aboutArtifact: about
15526
+ };
15527
+ allItemsById.set(item.id, item);
15528
+ if (about && sprintItemIds.has(about)) {
15529
+ if (!childrenByParent.has(about)) childrenByParent.set(about, []);
15530
+ childrenByParent.get(about).push(item);
15531
+ }
15532
+ }
15533
+ const itemsWithChildren = /* @__PURE__ */ new Set();
15534
+ for (const [parentId, children] of childrenByParent) {
15535
+ const parent = allItemsById.get(parentId);
15536
+ if (parent) {
15537
+ parent.children = children;
15538
+ for (const child of children) itemsWithChildren.add(child.id);
15539
+ }
15540
+ }
15541
+ for (const item of allItemsById.values()) {
15542
+ if (item.children) {
15543
+ for (const child of item.children) {
15544
+ const grandchildren = childrenByParent.get(child.id);
15545
+ if (grandchildren) {
15546
+ child.children = grandchildren;
15547
+ for (const gc of grandchildren) itemsWithChildren.add(gc.id);
15548
+ }
15549
+ }
15550
+ }
15551
+ }
15552
+ const items = [];
15553
+ for (const doc of workItemDocs) {
15554
+ if (!itemsWithChildren.has(doc.frontmatter.id)) {
15555
+ items.push(allItemsById.get(doc.frontmatter.id));
15556
+ }
15557
+ }
15490
15558
  const workItems = {
15491
- total: workItemDocs.length,
15559
+ total: primaryDocs.length,
15492
15560
  done: doneCount,
15493
15561
  inProgress: inProgressCount,
15494
15562
  open: openCount,
15495
15563
  blocked: blockedCount,
15496
- completionPct: workItemDocs.length > 0 ? Math.round(doneCount / workItemDocs.length * 100) : 0,
15564
+ completionPct: primaryDocs.length > 0 ? Math.round(doneCount / primaryDocs.length * 100) : 0,
15497
15565
  byStatus,
15498
15566
  byType,
15499
- items: workItemDocs.map((d) => ({
15500
- id: d.frontmatter.id,
15501
- title: d.frontmatter.title,
15502
- type: d.frontmatter.type,
15503
- status: d.frontmatter.status
15504
- }))
15567
+ items
15505
15568
  };
15506
15569
  const meetings = [];
15507
15570
  if (startDate && endDate) {
@@ -15583,7 +15646,7 @@ function collectSprintSummaryData(store, sprintId) {
15583
15646
  const prev = completedSprints[0];
15584
15647
  const prevTag = `sprint:${prev.frontmatter.id}`;
15585
15648
  const prevWorkItems = allDocs.filter(
15586
- (d) => d.frontmatter.type !== "sprint" && d.frontmatter.type !== "epic" && d.frontmatter.tags?.includes(prevTag)
15649
+ (d) => d.frontmatter.type !== "sprint" && d.frontmatter.type !== "epic" && d.frontmatter.type !== "contribution" && d.frontmatter.tags?.includes(prevTag)
15587
15650
  );
15588
15651
  const prevDone = prevWorkItems.filter((d) => DONE_STATUSES.has(d.frontmatter.status)).length;
15589
15652
  const prevRate = prevWorkItems.length > 0 ? Math.round(prevDone / prevWorkItems.length * 100) : 0;
@@ -16453,6 +16516,17 @@ tr:hover td {
16453
16516
  background: var(--bg-hover);
16454
16517
  }
16455
16518
 
16519
+ /* Hierarchical work-item sub-rows */
16520
+ .child-row td {
16521
+ font-size: 0.8125rem;
16522
+ border-bottom-style: dashed;
16523
+ }
16524
+ .contribution-row td {
16525
+ font-size: 0.8125rem;
16526
+ color: var(--text-dim);
16527
+ border-bottom-style: dashed;
16528
+ }
16529
+
16456
16530
  /* GAR */
16457
16531
  .gar-overall {
16458
16532
  text-align: center;
@@ -18140,22 +18214,36 @@ function sprintSummaryPage(data, cached2) {
18140
18214
  </div>`,
18141
18215
  { titleTag: "h3" }
18142
18216
  ) : "";
18143
- const workItemsSection = data.workItems.total > 0 ? collapsibleSection(
18217
+ function renderItemRows(items, depth = 0) {
18218
+ return items.flatMap((w) => {
18219
+ const isChild = depth > 0;
18220
+ const isContribution = w.type === "contribution";
18221
+ const rowClass = isContribution ? ' class="contribution-row"' : isChild ? ' class="child-row"' : "";
18222
+ const indent = depth > 0 ? ` style="padding-left: ${0.75 + depth * 1}rem"` : "";
18223
+ const streamCell = w.workStream ? `<span class="badge badge-subtle">${escapeHtml(w.workStream)}</span>` : "";
18224
+ const row = `
18225
+ <tr${rowClass}>
18226
+ <td${indent}><a href="/docs/${escapeHtml(w.type)}/${escapeHtml(w.id)}">${escapeHtml(w.id)}</a></td>
18227
+ <td>${escapeHtml(w.title)}</td>
18228
+ <td>${streamCell}</td>
18229
+ <td>${escapeHtml(typeLabel(w.type))}</td>
18230
+ <td>${statusBadge(w.status)}</td>
18231
+ </tr>`;
18232
+ const childRows = w.children ? renderItemRows(w.children, depth + 1) : [];
18233
+ return [row, ...childRows];
18234
+ });
18235
+ }
18236
+ const workItemRows = renderItemRows(data.workItems.items);
18237
+ const workItemsSection = workItemRows.length > 0 ? collapsibleSection(
18144
18238
  "ss-work-items",
18145
18239
  "Work Items",
18146
18240
  `<div class="table-wrap">
18147
18241
  <table>
18148
18242
  <thead>
18149
- <tr><th>ID</th><th>Title</th><th>Type</th><th>Status</th></tr>
18243
+ <tr><th>ID</th><th>Title</th><th>Stream</th><th>Type</th><th>Status</th></tr>
18150
18244
  </thead>
18151
18245
  <tbody>
18152
- ${data.workItems.items.map((w) => `
18153
- <tr>
18154
- <td><a href="/docs/${escapeHtml(w.type)}/${escapeHtml(w.id)}">${escapeHtml(w.id)}</a></td>
18155
- <td>${escapeHtml(w.title)}</td>
18156
- <td>${escapeHtml(typeLabel(w.type))}</td>
18157
- <td>${statusBadge(w.status)}</td>
18158
- </tr>`).join("")}
18246
+ ${workItemRows.join("")}
18159
18247
  </tbody>
18160
18248
  </table>
18161
18249
  </div>`,
@@ -19449,19 +19537,22 @@ function createContributionTools(store) {
19449
19537
  content: external_exports.string().describe("Contribution content \u2014 the input from the persona"),
19450
19538
  persona: external_exports.string().describe("Persona making the contribution (e.g. 'tech-lead')"),
19451
19539
  contributionType: external_exports.string().describe("Type of contribution (e.g. 'action-result', 'risk-finding')"),
19452
- aboutArtifact: external_exports.string().optional().describe("Artifact ID this contribution relates to (e.g. 'A-001')"),
19453
- status: external_exports.string().optional().describe("Status (default: 'open')"),
19454
- tags: external_exports.array(external_exports.string()).optional().describe("Tags for categorization")
19540
+ aboutArtifact: external_exports.string().describe("Artifact ID this contribution relates to (e.g. 'A-001', 'T-003')"),
19541
+ status: external_exports.string().optional().describe("Status (default: 'done')"),
19542
+ tags: external_exports.array(external_exports.string()).optional().describe("Tags for categorization"),
19543
+ workStream: external_exports.string().optional().describe("Work stream name (e.g. 'Budget UX'). Adds a stream:<value> tag.")
19455
19544
  },
19456
19545
  async (args) => {
19457
19546
  const frontmatter = {
19458
19547
  title: args.title,
19459
- status: args.status ?? "open",
19548
+ status: args.status ?? "done",
19460
19549
  persona: args.persona,
19461
19550
  contributionType: args.contributionType
19462
19551
  };
19463
- if (args.aboutArtifact) frontmatter.aboutArtifact = args.aboutArtifact;
19464
- if (args.tags) frontmatter.tags = args.tags;
19552
+ frontmatter.aboutArtifact = args.aboutArtifact;
19553
+ const tags = [...args.tags ?? []];
19554
+ if (args.workStream) tags.push(`stream:${args.workStream}`);
19555
+ if (tags.length > 0) frontmatter.tags = tags;
19465
19556
  const doc = store.create("contribution", frontmatter, args.content);
19466
19557
  return {
19467
19558
  content: [
@@ -19480,10 +19571,18 @@ function createContributionTools(store) {
19480
19571
  id: external_exports.string().describe("Contribution ID to update"),
19481
19572
  title: external_exports.string().optional().describe("New title"),
19482
19573
  status: external_exports.string().optional().describe("New status"),
19483
- content: external_exports.string().optional().describe("New content")
19574
+ content: external_exports.string().optional().describe("New content"),
19575
+ workStream: external_exports.string().optional().describe("Work stream name (e.g. 'Budget UX'). Replaces existing stream:<value> tag.")
19484
19576
  },
19485
19577
  async (args) => {
19486
- const { id, content, ...updates } = args;
19578
+ const { id, content, workStream, ...updates } = args;
19579
+ if (workStream !== void 0) {
19580
+ const existing = store.get(id);
19581
+ const existingTags = existing?.frontmatter.tags ?? [];
19582
+ const filtered = existingTags.filter((t) => !t.startsWith("stream:"));
19583
+ filtered.push(`stream:${workStream}`);
19584
+ updates.tags = filtered;
19585
+ }
19487
19586
  const doc = store.update(id, updates, content);
19488
19587
  return {
19489
19588
  content: [
@@ -19944,13 +20043,15 @@ function createTaskTools(store) {
19944
20043
  title: external_exports.string().describe("Task title"),
19945
20044
  content: external_exports.string().describe("Task description and implementation details"),
19946
20045
  linkedEpic: linkedEpicArray.describe("Epic ID(s) to link this task to (e.g. ['E-001'] or ['E-001', 'E-002'])"),
20046
+ aboutArtifact: external_exports.string().optional().describe("Parent artifact this task derives from (e.g. 'A-001')"),
19947
20047
  status: external_exports.enum(["backlog", "ready", "in-progress", "review", "done"]).optional().describe("Task status (default: 'backlog')"),
19948
20048
  acceptanceCriteria: external_exports.string().optional().describe("Acceptance criteria for the task"),
19949
20049
  technicalNotes: external_exports.string().optional().describe("Technical implementation notes"),
19950
20050
  estimatedPoints: external_exports.number().optional().describe("Story point estimate"),
19951
20051
  complexity: external_exports.enum(["trivial", "simple", "moderate", "complex", "very-complex"]).optional().describe("Task complexity"),
19952
20052
  priority: external_exports.enum(["critical", "high", "medium", "low"]).optional().describe("Task priority"),
19953
- tags: external_exports.array(external_exports.string()).optional().describe("Additional tags")
20053
+ tags: external_exports.array(external_exports.string()).optional().describe("Additional tags"),
20054
+ workStream: external_exports.string().optional().describe("Work stream name (e.g. 'Budget UX'). Adds a stream:<value> tag.")
19954
20055
  },
19955
20056
  async (args) => {
19956
20057
  const linkedEpics = normalizeLinkedEpics(args.linkedEpic);
@@ -19963,12 +20064,15 @@ function createTaskTools(store) {
19963
20064
  warnings.push(`Warning: ${epicId} is a ${epic.frontmatter.type}, not an epic`);
19964
20065
  }
19965
20066
  }
20067
+ const baseTags = [...generateEpicTags(linkedEpics), ...args.tags ?? []];
20068
+ if (args.workStream) baseTags.push(`stream:${args.workStream}`);
19966
20069
  const frontmatter = {
19967
20070
  title: args.title,
19968
20071
  status: args.status ?? "backlog",
19969
20072
  linkedEpic: linkedEpics,
19970
- tags: [...generateEpicTags(linkedEpics), ...args.tags ?? []]
20073
+ tags: baseTags
19971
20074
  };
20075
+ if (args.aboutArtifact) frontmatter.aboutArtifact = args.aboutArtifact;
19972
20076
  if (args.acceptanceCriteria) frontmatter.acceptanceCriteria = args.acceptanceCriteria;
19973
20077
  if (args.technicalNotes) frontmatter.technicalNotes = args.technicalNotes;
19974
20078
  if (args.estimatedPoints !== void 0) frontmatter.estimatedPoints = args.estimatedPoints;
@@ -19992,6 +20096,7 @@ function createTaskTools(store) {
19992
20096
  {
19993
20097
  id: external_exports.string().describe("Task ID to update"),
19994
20098
  title: external_exports.string().optional().describe("New title"),
20099
+ aboutArtifact: external_exports.string().optional().describe("Parent artifact this task derives from (e.g. 'A-001')"),
19995
20100
  status: external_exports.enum(["backlog", "ready", "in-progress", "review", "done"]).optional().describe("New status"),
19996
20101
  content: external_exports.string().optional().describe("New content"),
19997
20102
  linkedEpic: linkedEpicArray.optional().describe("New linked epic ID(s)"),
@@ -20000,10 +20105,11 @@ function createTaskTools(store) {
20000
20105
  estimatedPoints: external_exports.number().optional().describe("New story point estimate"),
20001
20106
  complexity: external_exports.enum(["trivial", "simple", "moderate", "complex", "very-complex"]).optional().describe("New complexity"),
20002
20107
  priority: external_exports.enum(["critical", "high", "medium", "low"]).optional().describe("New priority"),
20003
- tags: external_exports.array(external_exports.string()).optional().describe("Replace tags (e.g. remove old tags, add new ones)")
20108
+ tags: external_exports.array(external_exports.string()).optional().describe("Replace tags (e.g. remove old tags, add new ones)"),
20109
+ workStream: external_exports.string().optional().describe("Work stream name (e.g. 'Budget UX'). Replaces existing stream:<value> tag.")
20004
20110
  },
20005
20111
  async (args) => {
20006
- const { id, content, linkedEpic: rawLinkedEpic, tags: userTags, ...updates } = args;
20112
+ const { id, content, linkedEpic: rawLinkedEpic, tags: userTags, workStream, ...updates } = args;
20007
20113
  const warnings = [];
20008
20114
  if (rawLinkedEpic !== void 0) {
20009
20115
  const linkedEpics = normalizeLinkedEpics(rawLinkedEpic);
@@ -20024,6 +20130,12 @@ function createTaskTools(store) {
20024
20130
  } else if (userTags !== void 0) {
20025
20131
  updates.tags = userTags;
20026
20132
  }
20133
+ if (workStream !== void 0) {
20134
+ const currentTags = updates.tags ?? store.get(id)?.frontmatter.tags ?? [];
20135
+ const filtered = currentTags.filter((t) => !t.startsWith("stream:"));
20136
+ filtered.push(`stream:${workStream}`);
20137
+ updates.tags = filtered;
20138
+ }
20027
20139
  const doc = store.update(id, updates, content);
20028
20140
  const parts = [`Updated task ${doc.frontmatter.id}: ${doc.frontmatter.title}`];
20029
20141
  if (warnings.length > 0) {
@@ -26165,7 +26277,7 @@ function createProgram() {
26165
26277
  const program = new Command();
26166
26278
  program.name("marvin").description(
26167
26279
  "AI-powered product development assistant with Product Owner, Delivery Manager, and Technical Lead personas"
26168
- ).version("0.4.7");
26280
+ ).version("0.4.9");
26169
26281
  program.command("init").description("Initialize a new Marvin project in the current directory").action(async () => {
26170
26282
  await initCommand();
26171
26283
  });