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/README.md +15 -13
- package/dist/index.js +117 -59
- package/dist/index.js.map +1 -1
- package/dist/marvin-serve.js +116 -58
- package/dist/marvin-serve.js.map +1 -1
- package/dist/marvin.js +117 -59
- package/dist/marvin.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -109,24 +109,26 @@ Each persona has a tuned system prompt that shapes how Claude approaches your pr
|
|
|
109
109
|
Marvin enforces a structured product development workflow:
|
|
110
110
|
|
|
111
111
|
1. **Product Owner** defines features (`F-xxx`) as `draft`, then approves them when requirements are clear
|
|
112
|
-
2. **Tech Lead** breaks approved features into implementation epics (`E-xxx`) — the system **enforces** that epics can only be created against approved features
|
|
112
|
+
2. **Tech Lead** breaks approved features into implementation epics (`E-xxx`) — the system **enforces** that epics can only be created against approved features. An epic can link to **one or more features** (e.g. a cross-cutting epic spanning auth and profiles)
|
|
113
113
|
3. **Delivery Manager** creates sprints (`SP-xxx`) with goals and date boundaries, assigns epics to sprints, and tracks progress
|
|
114
114
|
|
|
115
115
|
```
|
|
116
|
-
Feature (PO) Epic (TL)
|
|
117
|
-
┌──────────┐
|
|
118
|
-
│ F-001 │───▶│ E-001
|
|
119
|
-
│ approved │ │ linked: F-001│ │ linkedEpics: [E-001] │
|
|
120
|
-
└──────────┘
|
|
121
|
-
│ E-002
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
116
|
+
Feature (PO) Epic (TL) Sprint (DM)
|
|
117
|
+
┌──────────┐ ┌────────────────────────┐ ┌──────────────────────┐
|
|
118
|
+
│ F-001 │───▶│ E-001 │───▶│ SP-001 │
|
|
119
|
+
│ approved │ │ linked: [F-001] │ │ linkedEpics: [E-001] │
|
|
120
|
+
└──────────┘ ├────────────────────────┤ │ goal: "Deliver auth" │
|
|
121
|
+
│ E-002 │ │ 2026-03-01..03-14 │
|
|
122
|
+
┌──────────┐───▶│ linked: [F-001, F-002] │ └──────────────────────┘
|
|
123
|
+
│ F-002 │ └────────────────────────┘ │
|
|
124
|
+
│ approved │ ┌────────┴─────────────┐
|
|
125
|
+
└──────────┘ │ A-001 (sprint:SP-001) │
|
|
126
|
+
│ D-003 (sprint:SP-001) │
|
|
127
|
+
└──────────────────────┘
|
|
128
128
|
```
|
|
129
129
|
|
|
130
|
+
Epics store `linkedFeature` as an array (e.g. `["F-001", "F-002"]`). Legacy files with a single string value are normalized to an array on read for backwards compatibility. Multi-linked epics appear in progress reports under each linked feature, and feature tags (`feature:F-xxx`) are generated for all linked features.
|
|
131
|
+
|
|
130
132
|
**Sprints** are time-boxed iterations with:
|
|
131
133
|
- `goal` — what the sprint aims to deliver
|
|
132
134
|
- `startDate` / `endDate` — sprint boundaries (ISO dates)
|
package/dist/index.js
CHANGED
|
@@ -15000,6 +15000,17 @@ function createSessionTools(store) {
|
|
|
15000
15000
|
import * as http2 from "http";
|
|
15001
15001
|
import { tool as tool20 } from "@anthropic-ai/claude-agent-sdk";
|
|
15002
15002
|
|
|
15003
|
+
// src/plugins/builtin/tools/epic-utils.ts
|
|
15004
|
+
function normalizeLinkedFeatures(value) {
|
|
15005
|
+
if (value === void 0 || value === null) return [];
|
|
15006
|
+
if (typeof value === "string") return [value];
|
|
15007
|
+
if (Array.isArray(value)) return value.filter((v) => typeof v === "string");
|
|
15008
|
+
return [];
|
|
15009
|
+
}
|
|
15010
|
+
function generateFeatureTags(features) {
|
|
15011
|
+
return features.map((id) => `feature:${id}`);
|
|
15012
|
+
}
|
|
15013
|
+
|
|
15003
15014
|
// src/reports/gar/collector.ts
|
|
15004
15015
|
function collectGarMetrics(store) {
|
|
15005
15016
|
const allActions = store.list({ type: "action" });
|
|
@@ -15472,7 +15483,7 @@ function getDiagramData(store) {
|
|
|
15472
15483
|
id: fm.id,
|
|
15473
15484
|
title: fm.title,
|
|
15474
15485
|
status: fm.status,
|
|
15475
|
-
linkedFeature: fm.linkedFeature
|
|
15486
|
+
linkedFeature: normalizeLinkedFeatures(fm.linkedFeature)
|
|
15476
15487
|
});
|
|
15477
15488
|
break;
|
|
15478
15489
|
case "feature":
|
|
@@ -16263,8 +16274,8 @@ function buildArtifactFlowchart(data) {
|
|
|
16263
16274
|
lines.push(" classDef default fill:#1e293b,stroke:#475569,color:#e2e8f0");
|
|
16264
16275
|
const nodeIds = /* @__PURE__ */ new Set();
|
|
16265
16276
|
for (const epic of data.epics) {
|
|
16266
|
-
|
|
16267
|
-
const feature = data.features.find((f) => f.id ===
|
|
16277
|
+
for (const featureId of epic.linkedFeature) {
|
|
16278
|
+
const feature = data.features.find((f) => f.id === featureId);
|
|
16268
16279
|
if (feature) {
|
|
16269
16280
|
const fNode = feature.id.replace(/-/g, "_");
|
|
16270
16281
|
const eNode = epic.id.replace(/-/g, "_");
|
|
@@ -17079,7 +17090,7 @@ function createReportTools(store) {
|
|
|
17079
17090
|
id: epicDoc.frontmatter.id,
|
|
17080
17091
|
title: epicDoc.frontmatter.title,
|
|
17081
17092
|
status: epicDoc.frontmatter.status,
|
|
17082
|
-
linkedFeature: epicDoc.frontmatter.linkedFeature,
|
|
17093
|
+
linkedFeature: normalizeLinkedFeatures(epicDoc.frontmatter.linkedFeature),
|
|
17083
17094
|
targetDate: epicDoc.frontmatter.targetDate,
|
|
17084
17095
|
estimatedEffort: epicDoc.frontmatter.estimatedEffort,
|
|
17085
17096
|
workItems: {
|
|
@@ -17206,7 +17217,7 @@ function createReportTools(store) {
|
|
|
17206
17217
|
const epicDocs = store.list({ type: "epic" });
|
|
17207
17218
|
const features = featureDocs.filter((f) => !args.feature || f.frontmatter.id === args.feature).map((f) => {
|
|
17208
17219
|
const linkedEpics = epicDocs.filter(
|
|
17209
|
-
(e) => e.frontmatter.linkedFeature
|
|
17220
|
+
(e) => normalizeLinkedFeatures(e.frontmatter.linkedFeature).includes(f.frontmatter.id)
|
|
17210
17221
|
);
|
|
17211
17222
|
const byStatus = {};
|
|
17212
17223
|
for (const e of linkedEpics) {
|
|
@@ -17410,14 +17421,14 @@ function createEpicTools(store) {
|
|
|
17410
17421
|
let docs = store.list({ type: "epic", status: args.status });
|
|
17411
17422
|
if (args.linkedFeature) {
|
|
17412
17423
|
docs = docs.filter(
|
|
17413
|
-
(d) => d.frontmatter.linkedFeature
|
|
17424
|
+
(d) => normalizeLinkedFeatures(d.frontmatter.linkedFeature).includes(args.linkedFeature)
|
|
17414
17425
|
);
|
|
17415
17426
|
}
|
|
17416
17427
|
const summary = docs.map((d) => ({
|
|
17417
17428
|
id: d.frontmatter.id,
|
|
17418
17429
|
title: d.frontmatter.title,
|
|
17419
17430
|
status: d.frontmatter.status,
|
|
17420
|
-
linkedFeature: d.frontmatter.linkedFeature,
|
|
17431
|
+
linkedFeature: normalizeLinkedFeatures(d.frontmatter.linkedFeature),
|
|
17421
17432
|
owner: d.frontmatter.owner,
|
|
17422
17433
|
targetDate: d.frontmatter.targetDate,
|
|
17423
17434
|
estimatedEffort: d.frontmatter.estimatedEffort,
|
|
@@ -17458,11 +17469,11 @@ function createEpicTools(store) {
|
|
|
17458
17469
|
),
|
|
17459
17470
|
tool10(
|
|
17460
17471
|
"create_epic",
|
|
17461
|
-
"Create a new epic linked to
|
|
17472
|
+
"Create a new epic linked to one or more approved features. All linked features must exist and be approved.",
|
|
17462
17473
|
{
|
|
17463
17474
|
title: external_exports.string().describe("Epic title"),
|
|
17464
17475
|
content: external_exports.string().describe("Epic description and scope"),
|
|
17465
|
-
linkedFeature: external_exports.string().describe("Feature ID to link this epic to (e.g. 'F-001')"),
|
|
17476
|
+
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'])"),
|
|
17466
17477
|
status: external_exports.enum(["planned", "in-progress", "done"]).optional().describe("Epic status (default: 'planned')"),
|
|
17467
17478
|
owner: external_exports.string().optional().describe("Epic owner"),
|
|
17468
17479
|
targetDate: external_exports.string().optional().describe("Target completion date (ISO format)"),
|
|
@@ -17470,45 +17481,48 @@ function createEpicTools(store) {
|
|
|
17470
17481
|
tags: external_exports.array(external_exports.string()).optional().describe("Additional tags")
|
|
17471
17482
|
},
|
|
17472
17483
|
async (args) => {
|
|
17473
|
-
const
|
|
17474
|
-
|
|
17475
|
-
|
|
17476
|
-
|
|
17477
|
-
|
|
17478
|
-
|
|
17479
|
-
|
|
17480
|
-
|
|
17481
|
-
|
|
17482
|
-
|
|
17483
|
-
|
|
17484
|
-
|
|
17485
|
-
|
|
17486
|
-
|
|
17487
|
-
|
|
17488
|
-
|
|
17489
|
-
|
|
17490
|
-
|
|
17491
|
-
|
|
17492
|
-
|
|
17493
|
-
|
|
17494
|
-
|
|
17495
|
-
|
|
17496
|
-
|
|
17497
|
-
|
|
17498
|
-
|
|
17499
|
-
|
|
17500
|
-
|
|
17501
|
-
|
|
17502
|
-
|
|
17503
|
-
|
|
17504
|
-
|
|
17505
|
-
|
|
17484
|
+
const linkedFeatures = normalizeLinkedFeatures(args.linkedFeature);
|
|
17485
|
+
for (const featureId of linkedFeatures) {
|
|
17486
|
+
const feature = store.get(featureId);
|
|
17487
|
+
if (!feature) {
|
|
17488
|
+
return {
|
|
17489
|
+
content: [
|
|
17490
|
+
{
|
|
17491
|
+
type: "text",
|
|
17492
|
+
text: `Feature ${featureId} not found`
|
|
17493
|
+
}
|
|
17494
|
+
],
|
|
17495
|
+
isError: true
|
|
17496
|
+
};
|
|
17497
|
+
}
|
|
17498
|
+
if (feature.frontmatter.type !== "feature") {
|
|
17499
|
+
return {
|
|
17500
|
+
content: [
|
|
17501
|
+
{
|
|
17502
|
+
type: "text",
|
|
17503
|
+
text: `${featureId} is a ${feature.frontmatter.type}, not a feature`
|
|
17504
|
+
}
|
|
17505
|
+
],
|
|
17506
|
+
isError: true
|
|
17507
|
+
};
|
|
17508
|
+
}
|
|
17509
|
+
if (feature.frontmatter.status !== "approved") {
|
|
17510
|
+
return {
|
|
17511
|
+
content: [
|
|
17512
|
+
{
|
|
17513
|
+
type: "text",
|
|
17514
|
+
text: `Feature ${featureId} has status '${feature.frontmatter.status}'. Only approved features can have epics. Ask the Product Owner to approve it first.`
|
|
17515
|
+
}
|
|
17516
|
+
],
|
|
17517
|
+
isError: true
|
|
17518
|
+
};
|
|
17519
|
+
}
|
|
17506
17520
|
}
|
|
17507
17521
|
const frontmatter = {
|
|
17508
17522
|
title: args.title,
|
|
17509
17523
|
status: args.status ?? "planned",
|
|
17510
|
-
linkedFeature:
|
|
17511
|
-
tags: [
|
|
17524
|
+
linkedFeature: linkedFeatures,
|
|
17525
|
+
tags: [...generateFeatureTags(linkedFeatures), ...args.tags ?? []]
|
|
17512
17526
|
};
|
|
17513
17527
|
if (args.owner) frontmatter.owner = args.owner;
|
|
17514
17528
|
if (args.targetDate) frontmatter.targetDate = args.targetDate;
|
|
@@ -17518,7 +17532,7 @@ function createEpicTools(store) {
|
|
|
17518
17532
|
content: [
|
|
17519
17533
|
{
|
|
17520
17534
|
type: "text",
|
|
17521
|
-
text: `Created epic ${doc.frontmatter.id}: ${doc.frontmatter.title} (linked to ${
|
|
17535
|
+
text: `Created epic ${doc.frontmatter.id}: ${doc.frontmatter.title} (linked to ${linkedFeatures.join(", ")})`
|
|
17522
17536
|
}
|
|
17523
17537
|
]
|
|
17524
17538
|
};
|
|
@@ -17526,7 +17540,7 @@ function createEpicTools(store) {
|
|
|
17526
17540
|
),
|
|
17527
17541
|
tool10(
|
|
17528
17542
|
"update_epic",
|
|
17529
|
-
"Update an existing epic
|
|
17543
|
+
"Update an existing epic, including its linked features.",
|
|
17530
17544
|
{
|
|
17531
17545
|
id: external_exports.string().describe("Epic ID to update"),
|
|
17532
17546
|
title: external_exports.string().optional().describe("New title"),
|
|
@@ -17535,10 +17549,49 @@ function createEpicTools(store) {
|
|
|
17535
17549
|
owner: external_exports.string().optional().describe("New owner"),
|
|
17536
17550
|
targetDate: external_exports.string().optional().describe("New target date"),
|
|
17537
17551
|
estimatedEffort: external_exports.string().optional().describe("New estimated effort"),
|
|
17552
|
+
linkedFeature: external_exports.union([external_exports.string(), external_exports.array(external_exports.string())]).optional().describe("New linked feature ID(s)"),
|
|
17538
17553
|
tags: external_exports.array(external_exports.string()).optional().describe("Replace tags (e.g. remove 'risk', add 'risk-mitigated')")
|
|
17539
17554
|
},
|
|
17540
17555
|
async (args) => {
|
|
17541
|
-
const { id, content, ...updates } = args;
|
|
17556
|
+
const { id, content, linkedFeature: rawLinkedFeature, tags: userTags, ...updates } = args;
|
|
17557
|
+
if (rawLinkedFeature !== void 0) {
|
|
17558
|
+
const linkedFeatures = normalizeLinkedFeatures(rawLinkedFeature);
|
|
17559
|
+
for (const featureId of linkedFeatures) {
|
|
17560
|
+
const feature = store.get(featureId);
|
|
17561
|
+
if (!feature) {
|
|
17562
|
+
return {
|
|
17563
|
+
content: [
|
|
17564
|
+
{ type: "text", text: `Feature ${featureId} not found` }
|
|
17565
|
+
],
|
|
17566
|
+
isError: true
|
|
17567
|
+
};
|
|
17568
|
+
}
|
|
17569
|
+
if (feature.frontmatter.type !== "feature") {
|
|
17570
|
+
return {
|
|
17571
|
+
content: [
|
|
17572
|
+
{ type: "text", text: `${featureId} is a ${feature.frontmatter.type}, not a feature` }
|
|
17573
|
+
],
|
|
17574
|
+
isError: true
|
|
17575
|
+
};
|
|
17576
|
+
}
|
|
17577
|
+
if (feature.frontmatter.status !== "approved") {
|
|
17578
|
+
return {
|
|
17579
|
+
content: [
|
|
17580
|
+
{ type: "text", text: `Feature ${featureId} has status '${feature.frontmatter.status}'. Only approved features can have epics. Ask the Product Owner to approve it first.` }
|
|
17581
|
+
],
|
|
17582
|
+
isError: true
|
|
17583
|
+
};
|
|
17584
|
+
}
|
|
17585
|
+
}
|
|
17586
|
+
updates.linkedFeature = linkedFeatures;
|
|
17587
|
+
const existingDoc = store.get(id);
|
|
17588
|
+
const existingTags = existingDoc?.frontmatter.tags ?? [];
|
|
17589
|
+
const nonFeatureTags = existingTags.filter((t) => !t.startsWith("feature:"));
|
|
17590
|
+
const baseTags = userTags ?? nonFeatureTags;
|
|
17591
|
+
updates.tags = [...generateFeatureTags(linkedFeatures), ...baseTags];
|
|
17592
|
+
} else if (userTags !== void 0) {
|
|
17593
|
+
updates.tags = userTags;
|
|
17594
|
+
}
|
|
17542
17595
|
const doc = store.update(id, updates, content);
|
|
17543
17596
|
return {
|
|
17544
17597
|
content: [
|
|
@@ -17873,7 +17926,9 @@ function createSprintPlanningTools(store) {
|
|
|
17873
17926
|
const questions = store.list({ type: "question", status: "open" });
|
|
17874
17927
|
const contributions = store.list({ type: "contribution" });
|
|
17875
17928
|
const approvedFeatures = features.filter((f) => f.frontmatter.status === "approved").sort((a, b) => priorityRank(a.frontmatter.priority) - priorityRank(b.frontmatter.priority)).map((f) => {
|
|
17876
|
-
const linkedEpics = epics.filter(
|
|
17929
|
+
const linkedEpics = epics.filter(
|
|
17930
|
+
(e) => normalizeLinkedFeatures(e.frontmatter.linkedFeature).includes(f.frontmatter.id)
|
|
17931
|
+
);
|
|
17877
17932
|
const epicsByStatus = {};
|
|
17878
17933
|
for (const e of linkedEpics) {
|
|
17879
17934
|
epicsByStatus[e.frontmatter.status] = (epicsByStatus[e.frontmatter.status] ?? 0) + 1;
|
|
@@ -17898,22 +17953,25 @@ function createSprintPlanningTools(store) {
|
|
|
17898
17953
|
);
|
|
17899
17954
|
if (args.focusFeature) {
|
|
17900
17955
|
backlogEpics = backlogEpics.filter(
|
|
17901
|
-
(e) => e.frontmatter.linkedFeature
|
|
17956
|
+
(e) => normalizeLinkedFeatures(e.frontmatter.linkedFeature).includes(args.focusFeature)
|
|
17902
17957
|
);
|
|
17903
17958
|
}
|
|
17904
17959
|
const backlog = backlogEpics.sort((a, b) => {
|
|
17905
|
-
const
|
|
17906
|
-
const
|
|
17907
|
-
|
|
17960
|
+
const aFeatures = normalizeLinkedFeatures(a.frontmatter.linkedFeature);
|
|
17961
|
+
const bFeatures = normalizeLinkedFeatures(b.frontmatter.linkedFeature);
|
|
17962
|
+
const aRank = Math.min(...aFeatures.map((id) => priorityRank(featureMap.get(id)?.frontmatter.priority)), 99);
|
|
17963
|
+
const bRank = Math.min(...bFeatures.map((id) => priorityRank(featureMap.get(id)?.frontmatter.priority)), 99);
|
|
17964
|
+
return aRank - bRank;
|
|
17908
17965
|
}).map((e) => {
|
|
17909
|
-
const
|
|
17966
|
+
const linkedFeatures = normalizeLinkedFeatures(e.frontmatter.linkedFeature);
|
|
17967
|
+
const parents = linkedFeatures.map((id) => featureMap.get(id)).filter(Boolean);
|
|
17910
17968
|
return {
|
|
17911
17969
|
id: e.frontmatter.id,
|
|
17912
17970
|
title: e.frontmatter.title,
|
|
17913
17971
|
status: e.frontmatter.status,
|
|
17914
|
-
linkedFeature:
|
|
17915
|
-
featureTitle:
|
|
17916
|
-
featurePriority:
|
|
17972
|
+
linkedFeature: linkedFeatures,
|
|
17973
|
+
featureTitle: parents.map((p) => p.frontmatter.title).join(", ") || null,
|
|
17974
|
+
featurePriority: parents.map((p) => p.frontmatter.priority).join(", ") || null,
|
|
17917
17975
|
estimatedEffort: e.frontmatter.estimatedEffort ?? null,
|
|
17918
17976
|
targetDate: e.frontmatter.targetDate ?? null
|
|
17919
17977
|
};
|
|
@@ -17994,8 +18052,8 @@ function createSprintPlanningTools(store) {
|
|
|
17994
18052
|
const epicsAtRisk = epics.filter((e) => {
|
|
17995
18053
|
if (e.frontmatter.status === "done") return false;
|
|
17996
18054
|
if (e.frontmatter.targetDate && e.frontmatter.targetDate < now) return true;
|
|
17997
|
-
const
|
|
17998
|
-
if (
|
|
18055
|
+
const linkedIds = normalizeLinkedFeatures(e.frontmatter.linkedFeature);
|
|
18056
|
+
if (linkedIds.some((id) => featureMap.get(id)?.frontmatter.status === "deferred")) return true;
|
|
17999
18057
|
return false;
|
|
18000
18058
|
}).map((e) => ({
|
|
18001
18059
|
id: e.frontmatter.id,
|
|
@@ -23650,7 +23708,7 @@ function createProgram() {
|
|
|
23650
23708
|
const program = new Command();
|
|
23651
23709
|
program.name("marvin").description(
|
|
23652
23710
|
"AI-powered product development assistant with Product Owner, Delivery Manager, and Technical Lead personas"
|
|
23653
|
-
).version("0.
|
|
23711
|
+
).version("0.4.0");
|
|
23654
23712
|
program.command("init").description("Initialize a new Marvin project in the current directory").action(async () => {
|
|
23655
23713
|
await initCommand();
|
|
23656
23714
|
});
|