mrvn-cli 0.3.3 → 0.3.6
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 +303 -147
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1278 -289
- package/dist/index.js.map +1 -1
- package/dist/marvin-serve.js +855 -89
- package/dist/marvin-serve.js.map +1 -1
- package/dist/marvin.js +1283 -296
- package/dist/marvin.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -432,6 +432,8 @@ var deliveryManager = {
|
|
|
432
432
|
|
|
433
433
|
## How You Work
|
|
434
434
|
- Review open actions (A-xxx) and follow up on overdue items
|
|
435
|
+
- Ensure every action has a dueDate \u2014 use update_action to backfill existing ones
|
|
436
|
+
- Assign actions to sprints when sprint planning is active, using the sprints parameter
|
|
435
437
|
- Ensure decisions (D-xxx) are properly documented with rationale
|
|
436
438
|
- Track questions (Q-xxx) and ensure they get answered
|
|
437
439
|
- Monitor project health and flag risks early
|
|
@@ -524,9 +526,24 @@ function resolvePersonaId(input4) {
|
|
|
524
526
|
}
|
|
525
527
|
|
|
526
528
|
// src/personas/prompt-builder.ts
|
|
527
|
-
|
|
529
|
+
import * as fs4 from "fs";
|
|
530
|
+
import * as path4 from "path";
|
|
531
|
+
function buildSystemPrompt(persona, projectConfig, pluginPromptFragment, skillPromptFragment, marvinDir) {
|
|
528
532
|
const parts = [];
|
|
529
533
|
parts.push(persona.systemPrompt);
|
|
534
|
+
if (marvinDir) {
|
|
535
|
+
const claudeMdPath = path4.join(marvinDir, "CLAUDE.md");
|
|
536
|
+
try {
|
|
537
|
+
const content = fs4.readFileSync(claudeMdPath, "utf-8").trim();
|
|
538
|
+
if (content) {
|
|
539
|
+
parts.push(`
|
|
540
|
+
## Project Instructions
|
|
541
|
+
${content}
|
|
542
|
+
`);
|
|
543
|
+
}
|
|
544
|
+
} catch {
|
|
545
|
+
}
|
|
546
|
+
}
|
|
530
547
|
parts.push(`
|
|
531
548
|
## Project Context
|
|
532
549
|
- **Project Name:** ${projectConfig.name}
|
|
@@ -536,7 +553,7 @@ function buildSystemPrompt(persona, projectConfig, pluginPromptFragment, skillPr
|
|
|
536
553
|
## Available Tools
|
|
537
554
|
You have access to governance tools for managing project artifacts:
|
|
538
555
|
- **Decisions** (D-xxx): List, get, create, and update decisions
|
|
539
|
-
- **Actions** (A-xxx): List, get, create, and update action items
|
|
556
|
+
- **Actions** (A-xxx): List, get, create, and update action items. Actions support \`dueDate\` for schedule tracking and \`sprints\` for sprint assignment.
|
|
540
557
|
- **Questions** (Q-xxx): List, get, create, and update questions
|
|
541
558
|
- **Features** (F-xxx): List, get, create, and update feature definitions
|
|
542
559
|
- **Epics** (E-xxx): List, get, create, and update implementation epics (must link to approved features)
|
|
@@ -1344,10 +1361,10 @@ function mergeDefs(...defs) {
|
|
|
1344
1361
|
function cloneDef(schema) {
|
|
1345
1362
|
return mergeDefs(schema._zod.def);
|
|
1346
1363
|
}
|
|
1347
|
-
function getElementAtPath(obj,
|
|
1348
|
-
if (!
|
|
1364
|
+
function getElementAtPath(obj, path20) {
|
|
1365
|
+
if (!path20)
|
|
1349
1366
|
return obj;
|
|
1350
|
-
return
|
|
1367
|
+
return path20.reduce((acc, key) => acc?.[key], obj);
|
|
1351
1368
|
}
|
|
1352
1369
|
function promiseAllObject(promisesObj) {
|
|
1353
1370
|
const keys = Object.keys(promisesObj);
|
|
@@ -1730,11 +1747,11 @@ function aborted(x, startIndex = 0) {
|
|
|
1730
1747
|
}
|
|
1731
1748
|
return false;
|
|
1732
1749
|
}
|
|
1733
|
-
function prefixIssues(
|
|
1750
|
+
function prefixIssues(path20, issues) {
|
|
1734
1751
|
return issues.map((iss) => {
|
|
1735
1752
|
var _a2;
|
|
1736
1753
|
(_a2 = iss).path ?? (_a2.path = []);
|
|
1737
|
-
iss.path.unshift(
|
|
1754
|
+
iss.path.unshift(path20);
|
|
1738
1755
|
return iss;
|
|
1739
1756
|
});
|
|
1740
1757
|
}
|
|
@@ -1917,7 +1934,7 @@ function formatError(error48, mapper = (issue2) => issue2.message) {
|
|
|
1917
1934
|
}
|
|
1918
1935
|
function treeifyError(error48, mapper = (issue2) => issue2.message) {
|
|
1919
1936
|
const result = { errors: [] };
|
|
1920
|
-
const processError = (error49,
|
|
1937
|
+
const processError = (error49, path20 = []) => {
|
|
1921
1938
|
var _a2, _b;
|
|
1922
1939
|
for (const issue2 of error49.issues) {
|
|
1923
1940
|
if (issue2.code === "invalid_union" && issue2.errors.length) {
|
|
@@ -1927,7 +1944,7 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
|
|
|
1927
1944
|
} else if (issue2.code === "invalid_element") {
|
|
1928
1945
|
processError({ issues: issue2.issues }, issue2.path);
|
|
1929
1946
|
} else {
|
|
1930
|
-
const fullpath = [...
|
|
1947
|
+
const fullpath = [...path20, ...issue2.path];
|
|
1931
1948
|
if (fullpath.length === 0) {
|
|
1932
1949
|
result.errors.push(mapper(issue2));
|
|
1933
1950
|
continue;
|
|
@@ -1959,8 +1976,8 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
|
|
|
1959
1976
|
}
|
|
1960
1977
|
function toDotPath(_path) {
|
|
1961
1978
|
const segs = [];
|
|
1962
|
-
const
|
|
1963
|
-
for (const seg of
|
|
1979
|
+
const path20 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
|
|
1980
|
+
for (const seg of path20) {
|
|
1964
1981
|
if (typeof seg === "number")
|
|
1965
1982
|
segs.push(`[${seg}]`);
|
|
1966
1983
|
else if (typeof seg === "symbol")
|
|
@@ -13937,13 +13954,13 @@ function resolveRef(ref, ctx) {
|
|
|
13937
13954
|
if (!ref.startsWith("#")) {
|
|
13938
13955
|
throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
|
|
13939
13956
|
}
|
|
13940
|
-
const
|
|
13941
|
-
if (
|
|
13957
|
+
const path20 = ref.slice(1).split("/").filter(Boolean);
|
|
13958
|
+
if (path20.length === 0) {
|
|
13942
13959
|
return ctx.rootSchema;
|
|
13943
13960
|
}
|
|
13944
13961
|
const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
|
|
13945
|
-
if (
|
|
13946
|
-
const key =
|
|
13962
|
+
if (path20[0] === defsKey) {
|
|
13963
|
+
const key = path20[1];
|
|
13947
13964
|
if (!key || !ctx.defs[key]) {
|
|
13948
13965
|
throw new Error(`Reference not found: ${ref}`);
|
|
13949
13966
|
}
|
|
@@ -14365,7 +14382,7 @@ function createDecisionTools(store) {
|
|
|
14365
14382
|
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
14366
14383
|
};
|
|
14367
14384
|
},
|
|
14368
|
-
{ annotations: {
|
|
14385
|
+
{ annotations: { readOnlyHint: true } }
|
|
14369
14386
|
),
|
|
14370
14387
|
tool(
|
|
14371
14388
|
"get_decision",
|
|
@@ -14392,7 +14409,7 @@ function createDecisionTools(store) {
|
|
|
14392
14409
|
]
|
|
14393
14410
|
};
|
|
14394
14411
|
},
|
|
14395
|
-
{ annotations: {
|
|
14412
|
+
{ annotations: { readOnlyHint: true } }
|
|
14396
14413
|
),
|
|
14397
14414
|
tool(
|
|
14398
14415
|
"create_decision",
|
|
@@ -14433,7 +14450,8 @@ function createDecisionTools(store) {
|
|
|
14433
14450
|
title: external_exports.string().optional().describe("New title"),
|
|
14434
14451
|
status: external_exports.string().optional().describe("New status"),
|
|
14435
14452
|
content: external_exports.string().optional().describe("New content"),
|
|
14436
|
-
owner: external_exports.string().optional().describe("New owner")
|
|
14453
|
+
owner: external_exports.string().optional().describe("New owner"),
|
|
14454
|
+
tags: external_exports.array(external_exports.string()).optional().describe("Replace tags (e.g. remove 'risk', add 'risk-mitigated')")
|
|
14437
14455
|
},
|
|
14438
14456
|
async (args) => {
|
|
14439
14457
|
const { id, content, ...updates } = args;
|
|
@@ -14453,6 +14471,19 @@ function createDecisionTools(store) {
|
|
|
14453
14471
|
|
|
14454
14472
|
// src/agent/tools/actions.ts
|
|
14455
14473
|
import { tool as tool2 } from "@anthropic-ai/claude-agent-sdk";
|
|
14474
|
+
function findMatchingSprints(store, dueDate) {
|
|
14475
|
+
const sprints = store.list({ type: "sprint" });
|
|
14476
|
+
return sprints.filter((s) => {
|
|
14477
|
+
const start = s.frontmatter.startDate;
|
|
14478
|
+
const end = s.frontmatter.endDate;
|
|
14479
|
+
return start && end && dueDate >= start && dueDate <= end;
|
|
14480
|
+
}).map((s) => ({
|
|
14481
|
+
id: s.frontmatter.id,
|
|
14482
|
+
title: s.frontmatter.title,
|
|
14483
|
+
startDate: s.frontmatter.startDate,
|
|
14484
|
+
endDate: s.frontmatter.endDate
|
|
14485
|
+
}));
|
|
14486
|
+
}
|
|
14456
14487
|
function createActionTools(store) {
|
|
14457
14488
|
return [
|
|
14458
14489
|
tool2(
|
|
@@ -14468,19 +14499,24 @@ function createActionTools(store) {
|
|
|
14468
14499
|
status: args.status,
|
|
14469
14500
|
owner: args.owner
|
|
14470
14501
|
});
|
|
14471
|
-
const summary = docs.map((d) =>
|
|
14472
|
-
|
|
14473
|
-
|
|
14474
|
-
|
|
14475
|
-
|
|
14476
|
-
|
|
14477
|
-
|
|
14478
|
-
|
|
14502
|
+
const summary = docs.map((d) => {
|
|
14503
|
+
const sprintIds = (d.frontmatter.tags ?? []).filter((t) => t.startsWith("sprint:")).map((t) => t.slice(7));
|
|
14504
|
+
return {
|
|
14505
|
+
id: d.frontmatter.id,
|
|
14506
|
+
title: d.frontmatter.title,
|
|
14507
|
+
status: d.frontmatter.status,
|
|
14508
|
+
owner: d.frontmatter.owner,
|
|
14509
|
+
priority: d.frontmatter.priority,
|
|
14510
|
+
dueDate: d.frontmatter.dueDate,
|
|
14511
|
+
sprints: sprintIds.length > 0 ? sprintIds : void 0,
|
|
14512
|
+
created: d.frontmatter.created
|
|
14513
|
+
};
|
|
14514
|
+
});
|
|
14479
14515
|
return {
|
|
14480
14516
|
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
14481
14517
|
};
|
|
14482
14518
|
},
|
|
14483
|
-
{ annotations: {
|
|
14519
|
+
{ annotations: { readOnlyHint: true } }
|
|
14484
14520
|
),
|
|
14485
14521
|
tool2(
|
|
14486
14522
|
"get_action",
|
|
@@ -14507,7 +14543,7 @@ function createActionTools(store) {
|
|
|
14507
14543
|
]
|
|
14508
14544
|
};
|
|
14509
14545
|
},
|
|
14510
|
-
{ annotations: {
|
|
14546
|
+
{ annotations: { readOnlyHint: true } }
|
|
14511
14547
|
),
|
|
14512
14548
|
tool2(
|
|
14513
14549
|
"create_action",
|
|
@@ -14518,9 +14554,18 @@ function createActionTools(store) {
|
|
|
14518
14554
|
status: external_exports.string().optional().describe("Status (default: 'open')"),
|
|
14519
14555
|
owner: external_exports.string().optional().describe("Person responsible"),
|
|
14520
14556
|
priority: external_exports.string().optional().describe("Priority (high, medium, low)"),
|
|
14521
|
-
tags: external_exports.array(external_exports.string()).optional().describe("Tags for categorization")
|
|
14557
|
+
tags: external_exports.array(external_exports.string()).optional().describe("Tags for categorization"),
|
|
14558
|
+
dueDate: external_exports.string().optional().describe("Due date in ISO format (e.g. '2026-03-15')"),
|
|
14559
|
+
sprints: external_exports.array(external_exports.string()).optional().describe("Sprint IDs to assign (e.g. ['SP-001']). Adds sprint:SP-xxx tags.")
|
|
14522
14560
|
},
|
|
14523
14561
|
async (args) => {
|
|
14562
|
+
const tags = [...args.tags ?? []];
|
|
14563
|
+
if (args.sprints) {
|
|
14564
|
+
for (const sprintId of args.sprints) {
|
|
14565
|
+
const tag = `sprint:${sprintId}`;
|
|
14566
|
+
if (!tags.includes(tag)) tags.push(tag);
|
|
14567
|
+
}
|
|
14568
|
+
}
|
|
14524
14569
|
const doc = store.create(
|
|
14525
14570
|
"action",
|
|
14526
14571
|
{
|
|
@@ -14528,17 +14573,21 @@ function createActionTools(store) {
|
|
|
14528
14573
|
status: args.status,
|
|
14529
14574
|
owner: args.owner,
|
|
14530
14575
|
priority: args.priority,
|
|
14531
|
-
tags:
|
|
14576
|
+
tags: tags.length > 0 ? tags : void 0,
|
|
14577
|
+
dueDate: args.dueDate
|
|
14532
14578
|
},
|
|
14533
14579
|
args.content
|
|
14534
14580
|
);
|
|
14581
|
+
const parts = [`Created action ${doc.frontmatter.id}: ${doc.frontmatter.title}`];
|
|
14582
|
+
if (args.dueDate && (!args.sprints || args.sprints.length === 0)) {
|
|
14583
|
+
const matching = findMatchingSprints(store, args.dueDate);
|
|
14584
|
+
if (matching.length > 0) {
|
|
14585
|
+
const suggestions = matching.map((s) => `${s.id} "${s.title}" (${s.startDate} \u2013 ${s.endDate})`).join(", ");
|
|
14586
|
+
parts.push(`Suggested sprints for dueDate ${args.dueDate}: ${suggestions}. Use the sprints parameter or update_action to assign.`);
|
|
14587
|
+
}
|
|
14588
|
+
}
|
|
14535
14589
|
return {
|
|
14536
|
-
content: [
|
|
14537
|
-
{
|
|
14538
|
-
type: "text",
|
|
14539
|
-
text: `Created action ${doc.frontmatter.id}: ${doc.frontmatter.title}`
|
|
14540
|
-
}
|
|
14541
|
-
]
|
|
14590
|
+
content: [{ type: "text", text: parts.join("\n") }]
|
|
14542
14591
|
};
|
|
14543
14592
|
}
|
|
14544
14593
|
),
|
|
@@ -14551,10 +14600,35 @@ function createActionTools(store) {
|
|
|
14551
14600
|
status: external_exports.string().optional().describe("New status"),
|
|
14552
14601
|
content: external_exports.string().optional().describe("New content"),
|
|
14553
14602
|
owner: external_exports.string().optional().describe("New owner"),
|
|
14554
|
-
priority: external_exports.string().optional().describe("New priority")
|
|
14603
|
+
priority: external_exports.string().optional().describe("New priority"),
|
|
14604
|
+
dueDate: external_exports.string().optional().describe("Due date in ISO format (e.g. '2026-03-15')"),
|
|
14605
|
+
tags: external_exports.array(external_exports.string()).optional().describe("Replace all tags. When provided with sprints, sprint tags are merged into this array."),
|
|
14606
|
+
sprints: external_exports.array(external_exports.string()).optional().describe("Sprint IDs to assign (replaces existing sprint tags). E.g. ['SP-001'].")
|
|
14555
14607
|
},
|
|
14556
14608
|
async (args) => {
|
|
14557
|
-
const { id, content, ...updates } = args;
|
|
14609
|
+
const { id, content, sprints, tags, ...updates } = args;
|
|
14610
|
+
if (tags !== void 0) {
|
|
14611
|
+
const merged = [...tags];
|
|
14612
|
+
if (sprints) {
|
|
14613
|
+
for (const s of sprints) {
|
|
14614
|
+
const tag = `sprint:${s}`;
|
|
14615
|
+
if (!merged.includes(tag)) merged.push(tag);
|
|
14616
|
+
}
|
|
14617
|
+
}
|
|
14618
|
+
updates.tags = merged;
|
|
14619
|
+
} else if (sprints !== void 0) {
|
|
14620
|
+
const existing = store.get(id);
|
|
14621
|
+
if (!existing) {
|
|
14622
|
+
return {
|
|
14623
|
+
content: [{ type: "text", text: `Action ${id} not found` }],
|
|
14624
|
+
isError: true
|
|
14625
|
+
};
|
|
14626
|
+
}
|
|
14627
|
+
const existingTags = existing.frontmatter.tags ?? [];
|
|
14628
|
+
const nonSprintTags = existingTags.filter((t) => !t.startsWith("sprint:"));
|
|
14629
|
+
const newSprintTags = sprints.map((s) => `sprint:${s}`);
|
|
14630
|
+
updates.tags = [...nonSprintTags, ...newSprintTags];
|
|
14631
|
+
}
|
|
14558
14632
|
const doc = store.update(id, updates, content);
|
|
14559
14633
|
return {
|
|
14560
14634
|
content: [
|
|
@@ -14565,6 +14639,35 @@ function createActionTools(store) {
|
|
|
14565
14639
|
]
|
|
14566
14640
|
};
|
|
14567
14641
|
}
|
|
14642
|
+
),
|
|
14643
|
+
tool2(
|
|
14644
|
+
"suggest_sprints_for_action",
|
|
14645
|
+
"Suggest sprints whose date range contains the given due date. Helps assign actions to the right sprint.",
|
|
14646
|
+
{
|
|
14647
|
+
dueDate: external_exports.string().describe("Due date in ISO format (e.g. '2026-03-15')")
|
|
14648
|
+
},
|
|
14649
|
+
async (args) => {
|
|
14650
|
+
const matching = findMatchingSprints(store, args.dueDate);
|
|
14651
|
+
if (matching.length === 0) {
|
|
14652
|
+
return {
|
|
14653
|
+
content: [
|
|
14654
|
+
{
|
|
14655
|
+
type: "text",
|
|
14656
|
+
text: `No sprints found containing dueDate ${args.dueDate}.`
|
|
14657
|
+
}
|
|
14658
|
+
]
|
|
14659
|
+
};
|
|
14660
|
+
}
|
|
14661
|
+
return {
|
|
14662
|
+
content: [
|
|
14663
|
+
{
|
|
14664
|
+
type: "text",
|
|
14665
|
+
text: JSON.stringify(matching, null, 2)
|
|
14666
|
+
}
|
|
14667
|
+
]
|
|
14668
|
+
};
|
|
14669
|
+
},
|
|
14670
|
+
{ annotations: { readOnlyHint: true } }
|
|
14568
14671
|
)
|
|
14569
14672
|
];
|
|
14570
14673
|
}
|
|
@@ -14592,7 +14695,7 @@ function createQuestionTools(store) {
|
|
|
14592
14695
|
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
14593
14696
|
};
|
|
14594
14697
|
},
|
|
14595
|
-
{ annotations: {
|
|
14698
|
+
{ annotations: { readOnlyHint: true } }
|
|
14596
14699
|
),
|
|
14597
14700
|
tool3(
|
|
14598
14701
|
"get_question",
|
|
@@ -14619,7 +14722,7 @@ function createQuestionTools(store) {
|
|
|
14619
14722
|
]
|
|
14620
14723
|
};
|
|
14621
14724
|
},
|
|
14622
|
-
{ annotations: {
|
|
14725
|
+
{ annotations: { readOnlyHint: true } }
|
|
14623
14726
|
),
|
|
14624
14727
|
tool3(
|
|
14625
14728
|
"create_question",
|
|
@@ -14660,7 +14763,8 @@ function createQuestionTools(store) {
|
|
|
14660
14763
|
title: external_exports.string().optional().describe("New title"),
|
|
14661
14764
|
status: external_exports.string().optional().describe("New status (e.g. 'answered')"),
|
|
14662
14765
|
content: external_exports.string().optional().describe("Updated content / answer"),
|
|
14663
|
-
owner: external_exports.string().optional().describe("New owner")
|
|
14766
|
+
owner: external_exports.string().optional().describe("New owner"),
|
|
14767
|
+
tags: external_exports.array(external_exports.string()).optional().describe("Replace tags (e.g. remove 'risk', add 'risk-mitigated')")
|
|
14664
14768
|
},
|
|
14665
14769
|
async (args) => {
|
|
14666
14770
|
const { id, content, ...updates } = args;
|
|
@@ -14712,7 +14816,7 @@ function createDocumentTools(store) {
|
|
|
14712
14816
|
]
|
|
14713
14817
|
};
|
|
14714
14818
|
},
|
|
14715
|
-
{ annotations: {
|
|
14819
|
+
{ annotations: { readOnlyHint: true } }
|
|
14716
14820
|
),
|
|
14717
14821
|
tool4(
|
|
14718
14822
|
"read_document",
|
|
@@ -14739,7 +14843,7 @@ function createDocumentTools(store) {
|
|
|
14739
14843
|
]
|
|
14740
14844
|
};
|
|
14741
14845
|
},
|
|
14742
|
-
{ annotations: {
|
|
14846
|
+
{ annotations: { readOnlyHint: true } }
|
|
14743
14847
|
),
|
|
14744
14848
|
tool4(
|
|
14745
14849
|
"project_summary",
|
|
@@ -14767,7 +14871,7 @@ function createDocumentTools(store) {
|
|
|
14767
14871
|
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
14768
14872
|
};
|
|
14769
14873
|
},
|
|
14770
|
-
{ annotations: {
|
|
14874
|
+
{ annotations: { readOnlyHint: true } }
|
|
14771
14875
|
)
|
|
14772
14876
|
];
|
|
14773
14877
|
}
|
|
@@ -14804,7 +14908,7 @@ function createSourceTools(manifest) {
|
|
|
14804
14908
|
]
|
|
14805
14909
|
};
|
|
14806
14910
|
},
|
|
14807
|
-
{ annotations: {
|
|
14911
|
+
{ annotations: { readOnlyHint: true } }
|
|
14808
14912
|
),
|
|
14809
14913
|
tool5(
|
|
14810
14914
|
"get_source_info",
|
|
@@ -14838,7 +14942,7 @@ function createSourceTools(manifest) {
|
|
|
14838
14942
|
]
|
|
14839
14943
|
};
|
|
14840
14944
|
},
|
|
14841
|
-
{ annotations: {
|
|
14945
|
+
{ annotations: { readOnlyHint: true } }
|
|
14842
14946
|
)
|
|
14843
14947
|
];
|
|
14844
14948
|
}
|
|
@@ -14869,7 +14973,7 @@ function createSessionTools(store) {
|
|
|
14869
14973
|
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
14870
14974
|
};
|
|
14871
14975
|
},
|
|
14872
|
-
{ annotations: {
|
|
14976
|
+
{ annotations: { readOnlyHint: true } }
|
|
14873
14977
|
),
|
|
14874
14978
|
tool6(
|
|
14875
14979
|
"get_session",
|
|
@@ -14887,7 +14991,7 @@ function createSessionTools(store) {
|
|
|
14887
14991
|
content: [{ type: "text", text: JSON.stringify(session, null, 2) }]
|
|
14888
14992
|
};
|
|
14889
14993
|
},
|
|
14890
|
-
{ annotations: {
|
|
14994
|
+
{ annotations: { readOnlyHint: true } }
|
|
14891
14995
|
)
|
|
14892
14996
|
];
|
|
14893
14997
|
}
|
|
@@ -14905,12 +15009,20 @@ function collectGarMetrics(store) {
|
|
|
14905
15009
|
const blockedItems = allDocs.filter(
|
|
14906
15010
|
(d) => d.frontmatter.tags?.includes("blocked")
|
|
14907
15011
|
);
|
|
14908
|
-
const
|
|
15012
|
+
const tagOverdueItems = allDocs.filter(
|
|
14909
15013
|
(d) => d.frontmatter.tags?.includes("overdue")
|
|
14910
15014
|
);
|
|
15015
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
15016
|
+
const dateOverdueActions = openActions.filter((d) => {
|
|
15017
|
+
const dueDate = d.frontmatter.dueDate;
|
|
15018
|
+
return typeof dueDate === "string" && dueDate < today;
|
|
15019
|
+
});
|
|
15020
|
+
const overdueItems = [...tagOverdueItems, ...dateOverdueActions].filter(
|
|
15021
|
+
(d, i, arr) => arr.findIndex((x) => x.frontmatter.id === d.frontmatter.id) === i
|
|
15022
|
+
);
|
|
14911
15023
|
const openQuestions = store.list({ type: "question", status: "open" });
|
|
14912
15024
|
const riskItems = allDocs.filter(
|
|
14913
|
-
(d) => d.frontmatter.tags?.includes("risk")
|
|
15025
|
+
(d) => d.frontmatter.tags?.includes("risk") && d.frontmatter.status !== "done" && d.frontmatter.status !== "closed"
|
|
14914
15026
|
);
|
|
14915
15027
|
const unownedActions = openActions.filter((d) => !d.frontmatter.owner);
|
|
14916
15028
|
const total = allActions.length;
|
|
@@ -15016,6 +15128,253 @@ function evaluateGar(projectName, metrics) {
|
|
|
15016
15128
|
};
|
|
15017
15129
|
}
|
|
15018
15130
|
|
|
15131
|
+
// src/reports/health/collector.ts
|
|
15132
|
+
var FIELD_CHECKS = [
|
|
15133
|
+
{
|
|
15134
|
+
type: "action",
|
|
15135
|
+
openStatuses: ["open", "in-progress"],
|
|
15136
|
+
requiredFields: ["owner", "priority", "dueDate", "content"]
|
|
15137
|
+
},
|
|
15138
|
+
{
|
|
15139
|
+
type: "decision",
|
|
15140
|
+
openStatuses: ["open", "proposed"],
|
|
15141
|
+
requiredFields: ["owner", "content"]
|
|
15142
|
+
},
|
|
15143
|
+
{
|
|
15144
|
+
type: "question",
|
|
15145
|
+
openStatuses: ["open"],
|
|
15146
|
+
requiredFields: ["owner", "content"]
|
|
15147
|
+
},
|
|
15148
|
+
{
|
|
15149
|
+
type: "feature",
|
|
15150
|
+
openStatuses: ["draft", "approved"],
|
|
15151
|
+
requiredFields: ["owner", "priority", "content"]
|
|
15152
|
+
},
|
|
15153
|
+
{
|
|
15154
|
+
type: "epic",
|
|
15155
|
+
openStatuses: ["planned", "in-progress"],
|
|
15156
|
+
requiredFields: ["owner", "targetDate", "estimatedEffort", "content"]
|
|
15157
|
+
},
|
|
15158
|
+
{
|
|
15159
|
+
type: "sprint",
|
|
15160
|
+
openStatuses: ["planned", "active"],
|
|
15161
|
+
requiredFields: ["goal", "startDate", "endDate", "linkedEpics"]
|
|
15162
|
+
}
|
|
15163
|
+
];
|
|
15164
|
+
var STALE_THRESHOLD_DAYS = 14;
|
|
15165
|
+
var AGING_THRESHOLD_DAYS = 30;
|
|
15166
|
+
function daysBetween(a, b) {
|
|
15167
|
+
const msPerDay = 864e5;
|
|
15168
|
+
const dateA = new Date(a);
|
|
15169
|
+
const dateB = new Date(b);
|
|
15170
|
+
return Math.floor(Math.abs(dateB.getTime() - dateA.getTime()) / msPerDay);
|
|
15171
|
+
}
|
|
15172
|
+
function checkMissingFields(doc, requiredFields) {
|
|
15173
|
+
const missing = [];
|
|
15174
|
+
for (const field of requiredFields) {
|
|
15175
|
+
if (field === "content") {
|
|
15176
|
+
if (!doc.content || doc.content.trim().length === 0) {
|
|
15177
|
+
missing.push("content");
|
|
15178
|
+
}
|
|
15179
|
+
} else if (field === "linkedEpics") {
|
|
15180
|
+
const val = doc.frontmatter[field];
|
|
15181
|
+
if (!Array.isArray(val) || val.length === 0) {
|
|
15182
|
+
missing.push(field);
|
|
15183
|
+
}
|
|
15184
|
+
} else {
|
|
15185
|
+
const val = doc.frontmatter[field];
|
|
15186
|
+
if (val === void 0 || val === null || val === "") {
|
|
15187
|
+
missing.push(field);
|
|
15188
|
+
}
|
|
15189
|
+
}
|
|
15190
|
+
}
|
|
15191
|
+
return missing;
|
|
15192
|
+
}
|
|
15193
|
+
function collectCompleteness(store) {
|
|
15194
|
+
const result = {};
|
|
15195
|
+
for (const check2 of FIELD_CHECKS) {
|
|
15196
|
+
const allOfType = store.list({ type: check2.type });
|
|
15197
|
+
const openDocs = allOfType.filter(
|
|
15198
|
+
(d) => check2.openStatuses.includes(d.frontmatter.status)
|
|
15199
|
+
);
|
|
15200
|
+
const gaps = [];
|
|
15201
|
+
let complete = 0;
|
|
15202
|
+
for (const doc of openDocs) {
|
|
15203
|
+
const missingFields = checkMissingFields(doc, check2.requiredFields);
|
|
15204
|
+
if (missingFields.length === 0) {
|
|
15205
|
+
complete++;
|
|
15206
|
+
} else {
|
|
15207
|
+
gaps.push({
|
|
15208
|
+
id: doc.frontmatter.id,
|
|
15209
|
+
title: doc.frontmatter.title,
|
|
15210
|
+
missingFields
|
|
15211
|
+
});
|
|
15212
|
+
}
|
|
15213
|
+
}
|
|
15214
|
+
result[check2.type] = {
|
|
15215
|
+
total: openDocs.length,
|
|
15216
|
+
complete,
|
|
15217
|
+
gaps
|
|
15218
|
+
};
|
|
15219
|
+
}
|
|
15220
|
+
return result;
|
|
15221
|
+
}
|
|
15222
|
+
function collectProcess(store) {
|
|
15223
|
+
const today = (/* @__PURE__ */ new Date()).toISOString();
|
|
15224
|
+
const allDocs = store.list();
|
|
15225
|
+
const openStatuses = new Set(FIELD_CHECKS.flatMap((c) => c.openStatuses));
|
|
15226
|
+
const openDocs = allDocs.filter((d) => openStatuses.has(d.frontmatter.status));
|
|
15227
|
+
const stale = [];
|
|
15228
|
+
for (const doc of openDocs) {
|
|
15229
|
+
const updated = doc.frontmatter.updated ?? doc.frontmatter.created;
|
|
15230
|
+
const days = daysBetween(updated, today);
|
|
15231
|
+
if (days >= STALE_THRESHOLD_DAYS) {
|
|
15232
|
+
stale.push({ id: doc.frontmatter.id, title: doc.frontmatter.title, days });
|
|
15233
|
+
}
|
|
15234
|
+
}
|
|
15235
|
+
const openActions = store.list({ type: "action" }).filter((d) => d.frontmatter.status === "open" || d.frontmatter.status === "in-progress");
|
|
15236
|
+
const agingActions = [];
|
|
15237
|
+
for (const doc of openActions) {
|
|
15238
|
+
const days = daysBetween(doc.frontmatter.created, today);
|
|
15239
|
+
if (days >= AGING_THRESHOLD_DAYS) {
|
|
15240
|
+
agingActions.push({ id: doc.frontmatter.id, title: doc.frontmatter.title, days });
|
|
15241
|
+
}
|
|
15242
|
+
}
|
|
15243
|
+
const resolvedDecisions = store.list({ type: "decision" }).filter((d) => !["open", "proposed"].includes(d.frontmatter.status));
|
|
15244
|
+
let decisionTotal = 0;
|
|
15245
|
+
for (const doc of resolvedDecisions) {
|
|
15246
|
+
decisionTotal += daysBetween(doc.frontmatter.created, doc.frontmatter.updated);
|
|
15247
|
+
}
|
|
15248
|
+
const decisionVelocity = {
|
|
15249
|
+
avgDays: resolvedDecisions.length > 0 ? Math.round(decisionTotal / resolvedDecisions.length) : 0,
|
|
15250
|
+
count: resolvedDecisions.length
|
|
15251
|
+
};
|
|
15252
|
+
const answeredQuestions = store.list({ type: "question" }).filter((d) => d.frontmatter.status !== "open");
|
|
15253
|
+
let questionTotal = 0;
|
|
15254
|
+
for (const doc of answeredQuestions) {
|
|
15255
|
+
questionTotal += daysBetween(doc.frontmatter.created, doc.frontmatter.updated);
|
|
15256
|
+
}
|
|
15257
|
+
const questionResolution = {
|
|
15258
|
+
avgDays: answeredQuestions.length > 0 ? Math.round(questionTotal / answeredQuestions.length) : 0,
|
|
15259
|
+
count: answeredQuestions.length
|
|
15260
|
+
};
|
|
15261
|
+
return { stale, agingActions, decisionVelocity, questionResolution };
|
|
15262
|
+
}
|
|
15263
|
+
function collectHealthMetrics(store) {
|
|
15264
|
+
return {
|
|
15265
|
+
completeness: collectCompleteness(store),
|
|
15266
|
+
process: collectProcess(store)
|
|
15267
|
+
};
|
|
15268
|
+
}
|
|
15269
|
+
|
|
15270
|
+
// src/reports/health/evaluator.ts
|
|
15271
|
+
function worstStatus2(statuses) {
|
|
15272
|
+
if (statuses.includes("red")) return "red";
|
|
15273
|
+
if (statuses.includes("amber")) return "amber";
|
|
15274
|
+
return "green";
|
|
15275
|
+
}
|
|
15276
|
+
function completenessStatus(total, complete) {
|
|
15277
|
+
if (total === 0) return "green";
|
|
15278
|
+
const pct = Math.round(complete / total * 100);
|
|
15279
|
+
if (pct >= 100) return "green";
|
|
15280
|
+
if (pct >= 75) return "amber";
|
|
15281
|
+
return "red";
|
|
15282
|
+
}
|
|
15283
|
+
var TYPE_LABELS = {
|
|
15284
|
+
action: "Actions",
|
|
15285
|
+
decision: "Decisions",
|
|
15286
|
+
question: "Questions",
|
|
15287
|
+
feature: "Features",
|
|
15288
|
+
epic: "Epics",
|
|
15289
|
+
sprint: "Sprints"
|
|
15290
|
+
};
|
|
15291
|
+
function evaluateHealth(projectName, metrics) {
|
|
15292
|
+
const completeness = [];
|
|
15293
|
+
for (const [type, catMetrics] of Object.entries(metrics.completeness)) {
|
|
15294
|
+
const { total, complete, gaps } = catMetrics;
|
|
15295
|
+
const status = completenessStatus(total, complete);
|
|
15296
|
+
const pct = total > 0 ? Math.round(complete / total * 100) : 100;
|
|
15297
|
+
completeness.push({
|
|
15298
|
+
name: TYPE_LABELS[type] ?? type,
|
|
15299
|
+
status,
|
|
15300
|
+
summary: `${pct}% complete (${complete}/${total})`,
|
|
15301
|
+
items: gaps.map((g) => ({
|
|
15302
|
+
id: g.id,
|
|
15303
|
+
detail: `missing: ${g.missingFields.join(", ")}`
|
|
15304
|
+
}))
|
|
15305
|
+
});
|
|
15306
|
+
}
|
|
15307
|
+
const process3 = [];
|
|
15308
|
+
const staleCount = metrics.process.stale.length;
|
|
15309
|
+
const staleStatus = staleCount === 0 ? "green" : staleCount <= 3 ? "amber" : "red";
|
|
15310
|
+
process3.push({
|
|
15311
|
+
name: "Stale Items",
|
|
15312
|
+
status: staleStatus,
|
|
15313
|
+
summary: staleCount === 0 ? "no stale items" : `${staleCount} item(s) not updated in 14+ days`,
|
|
15314
|
+
items: metrics.process.stale.map((s) => ({
|
|
15315
|
+
id: s.id,
|
|
15316
|
+
detail: `${s.days} days since last update`
|
|
15317
|
+
}))
|
|
15318
|
+
});
|
|
15319
|
+
const agingCount = metrics.process.agingActions.length;
|
|
15320
|
+
const agingStatus = agingCount === 0 ? "green" : agingCount <= 3 ? "amber" : "red";
|
|
15321
|
+
process3.push({
|
|
15322
|
+
name: "Aging Actions",
|
|
15323
|
+
status: agingStatus,
|
|
15324
|
+
summary: agingCount === 0 ? "no aging actions" : `${agingCount} action(s) open for 30+ days`,
|
|
15325
|
+
items: metrics.process.agingActions.map((a) => ({
|
|
15326
|
+
id: a.id,
|
|
15327
|
+
detail: `open for ${a.days} days`
|
|
15328
|
+
}))
|
|
15329
|
+
});
|
|
15330
|
+
const dv = metrics.process.decisionVelocity;
|
|
15331
|
+
let dvStatus;
|
|
15332
|
+
if (dv.count === 0) {
|
|
15333
|
+
dvStatus = "green";
|
|
15334
|
+
} else if (dv.avgDays <= 7) {
|
|
15335
|
+
dvStatus = "green";
|
|
15336
|
+
} else if (dv.avgDays <= 21) {
|
|
15337
|
+
dvStatus = "amber";
|
|
15338
|
+
} else {
|
|
15339
|
+
dvStatus = "red";
|
|
15340
|
+
}
|
|
15341
|
+
process3.push({
|
|
15342
|
+
name: "Decision Velocity",
|
|
15343
|
+
status: dvStatus,
|
|
15344
|
+
summary: dv.count === 0 ? "no resolved decisions" : `avg ${dv.avgDays} days to resolve (${dv.count} decision(s))`,
|
|
15345
|
+
items: []
|
|
15346
|
+
});
|
|
15347
|
+
const qr = metrics.process.questionResolution;
|
|
15348
|
+
let qrStatus;
|
|
15349
|
+
if (qr.count === 0) {
|
|
15350
|
+
qrStatus = "green";
|
|
15351
|
+
} else if (qr.avgDays <= 7) {
|
|
15352
|
+
qrStatus = "green";
|
|
15353
|
+
} else if (qr.avgDays <= 14) {
|
|
15354
|
+
qrStatus = "amber";
|
|
15355
|
+
} else {
|
|
15356
|
+
qrStatus = "red";
|
|
15357
|
+
}
|
|
15358
|
+
process3.push({
|
|
15359
|
+
name: "Question Resolution",
|
|
15360
|
+
status: qrStatus,
|
|
15361
|
+
summary: qr.count === 0 ? "no answered questions" : `avg ${qr.avgDays} days to answer (${qr.count} question(s))`,
|
|
15362
|
+
items: []
|
|
15363
|
+
});
|
|
15364
|
+
const allStatuses = [
|
|
15365
|
+
...completeness.map((c) => c.status),
|
|
15366
|
+
...process3.map((p) => p.status)
|
|
15367
|
+
];
|
|
15368
|
+
const overall = worstStatus2(allStatuses);
|
|
15369
|
+
return {
|
|
15370
|
+
projectName,
|
|
15371
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
15372
|
+
overall,
|
|
15373
|
+
completeness,
|
|
15374
|
+
process: process3
|
|
15375
|
+
};
|
|
15376
|
+
}
|
|
15377
|
+
|
|
15019
15378
|
// src/web/data.ts
|
|
15020
15379
|
function getOverviewData(store) {
|
|
15021
15380
|
const types = [];
|
|
@@ -15087,6 +15446,46 @@ function getBoardData(store, type) {
|
|
|
15087
15446
|
}));
|
|
15088
15447
|
return { columns, type, types };
|
|
15089
15448
|
}
|
|
15449
|
+
function getDiagramData(store) {
|
|
15450
|
+
const allDocs = store.list();
|
|
15451
|
+
const sprints = [];
|
|
15452
|
+
const epics = [];
|
|
15453
|
+
const features = [];
|
|
15454
|
+
const statusCounts = {};
|
|
15455
|
+
for (const doc of allDocs) {
|
|
15456
|
+
const fm = doc.frontmatter;
|
|
15457
|
+
const status = fm.status.toLowerCase();
|
|
15458
|
+
statusCounts[status] = (statusCounts[status] ?? 0) + 1;
|
|
15459
|
+
switch (fm.type) {
|
|
15460
|
+
case "sprint":
|
|
15461
|
+
sprints.push({
|
|
15462
|
+
id: fm.id,
|
|
15463
|
+
title: fm.title,
|
|
15464
|
+
status: fm.status,
|
|
15465
|
+
startDate: fm.startDate,
|
|
15466
|
+
endDate: fm.endDate,
|
|
15467
|
+
linkedEpics: fm.linkedEpics ?? []
|
|
15468
|
+
});
|
|
15469
|
+
break;
|
|
15470
|
+
case "epic":
|
|
15471
|
+
epics.push({
|
|
15472
|
+
id: fm.id,
|
|
15473
|
+
title: fm.title,
|
|
15474
|
+
status: fm.status,
|
|
15475
|
+
linkedFeature: fm.linkedFeature
|
|
15476
|
+
});
|
|
15477
|
+
break;
|
|
15478
|
+
case "feature":
|
|
15479
|
+
features.push({
|
|
15480
|
+
id: fm.id,
|
|
15481
|
+
title: fm.title,
|
|
15482
|
+
status: fm.status
|
|
15483
|
+
});
|
|
15484
|
+
break;
|
|
15485
|
+
}
|
|
15486
|
+
}
|
|
15487
|
+
return { sprints, epics, features, statusCounts };
|
|
15488
|
+
}
|
|
15090
15489
|
|
|
15091
15490
|
// src/web/templates/layout.ts
|
|
15092
15491
|
function escapeHtml(str) {
|
|
@@ -15117,16 +15516,43 @@ function renderMarkdown(md) {
|
|
|
15117
15516
|
const out = [];
|
|
15118
15517
|
let inList = false;
|
|
15119
15518
|
let listTag = "ul";
|
|
15120
|
-
|
|
15121
|
-
|
|
15519
|
+
let inTable = false;
|
|
15520
|
+
let i = 0;
|
|
15521
|
+
while (i < lines.length) {
|
|
15522
|
+
const line = lines[i];
|
|
15122
15523
|
if (inList && !/^\s*[-*]\s/.test(line) && !/^\s*\d+\.\s/.test(line) && line.trim() !== "") {
|
|
15123
15524
|
out.push(`</${listTag}>`);
|
|
15124
15525
|
inList = false;
|
|
15125
15526
|
}
|
|
15527
|
+
if (inTable && !/^\s*\|/.test(line)) {
|
|
15528
|
+
out.push("</tbody></table></div>");
|
|
15529
|
+
inTable = false;
|
|
15530
|
+
}
|
|
15531
|
+
if (/^(-{3,}|\*{3,}|_{3,})\s*$/.test(line.trim())) {
|
|
15532
|
+
i++;
|
|
15533
|
+
out.push("<hr>");
|
|
15534
|
+
continue;
|
|
15535
|
+
}
|
|
15536
|
+
if (!inTable && /^\s*\|/.test(line) && i + 1 < lines.length && /^\s*\|[\s:|-]+\|\s*$/.test(lines[i + 1])) {
|
|
15537
|
+
const headers = parseTableRow(line);
|
|
15538
|
+
out.push('<div class="table-wrap"><table><thead><tr>');
|
|
15539
|
+
out.push(headers.map((h) => `<th>${inline(h)}</th>`).join(""));
|
|
15540
|
+
out.push("</tr></thead><tbody>");
|
|
15541
|
+
inTable = true;
|
|
15542
|
+
i += 2;
|
|
15543
|
+
continue;
|
|
15544
|
+
}
|
|
15545
|
+
if (inTable && /^\s*\|/.test(line)) {
|
|
15546
|
+
const cells = parseTableRow(line);
|
|
15547
|
+
out.push("<tr>" + cells.map((c) => `<td>${inline(c)}</td>`).join("") + "</tr>");
|
|
15548
|
+
i++;
|
|
15549
|
+
continue;
|
|
15550
|
+
}
|
|
15126
15551
|
const headingMatch = line.match(/^(#{1,3})\s+(.+)$/);
|
|
15127
15552
|
if (headingMatch) {
|
|
15128
15553
|
const level = headingMatch[1].length;
|
|
15129
15554
|
out.push(`<h${level}>${inline(headingMatch[2])}</h${level}>`);
|
|
15555
|
+
i++;
|
|
15130
15556
|
continue;
|
|
15131
15557
|
}
|
|
15132
15558
|
const ulMatch = line.match(/^\s*[-*]\s+(.+)$/);
|
|
@@ -15138,6 +15564,7 @@ function renderMarkdown(md) {
|
|
|
15138
15564
|
listTag = "ul";
|
|
15139
15565
|
}
|
|
15140
15566
|
out.push(`<li>${inline(ulMatch[1])}</li>`);
|
|
15567
|
+
i++;
|
|
15141
15568
|
continue;
|
|
15142
15569
|
}
|
|
15143
15570
|
const olMatch = line.match(/^\s*\d+\.\s+(.+)$/);
|
|
@@ -15149,6 +15576,7 @@ function renderMarkdown(md) {
|
|
|
15149
15576
|
listTag = "ol";
|
|
15150
15577
|
}
|
|
15151
15578
|
out.push(`<li>${inline(olMatch[1])}</li>`);
|
|
15579
|
+
i++;
|
|
15152
15580
|
continue;
|
|
15153
15581
|
}
|
|
15154
15582
|
if (line.trim() === "") {
|
|
@@ -15156,13 +15584,19 @@ function renderMarkdown(md) {
|
|
|
15156
15584
|
out.push(`</${listTag}>`);
|
|
15157
15585
|
inList = false;
|
|
15158
15586
|
}
|
|
15587
|
+
i++;
|
|
15159
15588
|
continue;
|
|
15160
15589
|
}
|
|
15161
15590
|
out.push(`<p>${inline(line)}</p>`);
|
|
15591
|
+
i++;
|
|
15162
15592
|
}
|
|
15163
15593
|
if (inList) out.push(`</${listTag}>`);
|
|
15594
|
+
if (inTable) out.push("</tbody></table></div>");
|
|
15164
15595
|
return out.join("\n");
|
|
15165
15596
|
}
|
|
15597
|
+
function parseTableRow(line) {
|
|
15598
|
+
return line.replace(/^\s*\|/, "").replace(/\|\s*$/, "").split("|").map((cell) => cell.trim());
|
|
15599
|
+
}
|
|
15166
15600
|
function inline(text) {
|
|
15167
15601
|
let s = escapeHtml(text);
|
|
15168
15602
|
s = s.replace(/`([^`]+)`/g, "<code>$1</code>");
|
|
@@ -15176,7 +15610,8 @@ function layout(opts, body) {
|
|
|
15176
15610
|
const topItems = [
|
|
15177
15611
|
{ href: "/", label: "Overview" },
|
|
15178
15612
|
{ href: "/board", label: "Board" },
|
|
15179
|
-
{ href: "/gar", label: "GAR Report" }
|
|
15613
|
+
{ href: "/gar", label: "GAR Report" },
|
|
15614
|
+
{ href: "/health", label: "Health" }
|
|
15180
15615
|
];
|
|
15181
15616
|
const isActive = (href) => opts.activePath === href || href !== "/" && opts.activePath.startsWith(href) ? " active" : "";
|
|
15182
15617
|
const groupsHtml = opts.navGroups.map((group) => {
|
|
@@ -15211,9 +15646,15 @@ function layout(opts, body) {
|
|
|
15211
15646
|
</nav>
|
|
15212
15647
|
</aside>
|
|
15213
15648
|
<main class="main">
|
|
15649
|
+
<button class="expand-toggle" onclick="document.querySelector('.main').classList.toggle('expanded')" title="Toggle wide view">
|
|
15650
|
+
<svg class="icon-expand" viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M1 1h5v1.5H3.56l3.72 3.72-1.06 1.06L2.5 3.56V6H1V1zm14 14h-5v-1.5h2.44l-3.72-3.72 1.06-1.06 3.72 3.72V10H15v5z"/></svg>
|
|
15651
|
+
<svg class="icon-collapse" viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M6 7H1V5.5h2.44L0.22 2.28l1.06-1.06L4.5 4.44V2H6v5zm4-1h5v1.5h-2.44l3.22 3.22-1.06 1.06L11.5 8.56V11H10V6z"/></svg>
|
|
15652
|
+
</button>
|
|
15214
15653
|
${body}
|
|
15215
15654
|
</main>
|
|
15216
15655
|
</div>
|
|
15656
|
+
<script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
|
|
15657
|
+
<script>mermaid.initialize({ startOnLoad: true, theme: 'dark' });</script>
|
|
15217
15658
|
</body>
|
|
15218
15659
|
</html>`;
|
|
15219
15660
|
}
|
|
@@ -15328,7 +15769,33 @@ a:hover { text-decoration: underline; }
|
|
|
15328
15769
|
flex: 1;
|
|
15329
15770
|
padding: 2rem 2.5rem;
|
|
15330
15771
|
max-width: 1200px;
|
|
15772
|
+
position: relative;
|
|
15773
|
+
transition: max-width 0.2s ease;
|
|
15774
|
+
}
|
|
15775
|
+
.main.expanded {
|
|
15776
|
+
max-width: none;
|
|
15777
|
+
}
|
|
15778
|
+
.expand-toggle {
|
|
15779
|
+
position: absolute;
|
|
15780
|
+
top: 1rem;
|
|
15781
|
+
right: 1rem;
|
|
15782
|
+
background: var(--bg-card);
|
|
15783
|
+
border: 1px solid var(--border);
|
|
15784
|
+
border-radius: var(--radius);
|
|
15785
|
+
color: var(--text-dim);
|
|
15786
|
+
cursor: pointer;
|
|
15787
|
+
padding: 0.4rem;
|
|
15788
|
+
display: flex;
|
|
15789
|
+
align-items: center;
|
|
15790
|
+
justify-content: center;
|
|
15791
|
+
transition: color 0.15s, border-color 0.15s;
|
|
15331
15792
|
}
|
|
15793
|
+
.expand-toggle:hover {
|
|
15794
|
+
color: var(--text);
|
|
15795
|
+
border-color: var(--text-dim);
|
|
15796
|
+
}
|
|
15797
|
+
.main.expanded .icon-expand { display: none; }
|
|
15798
|
+
.main:not(.expanded) .icon-collapse { display: none; }
|
|
15332
15799
|
|
|
15333
15800
|
/* Page header */
|
|
15334
15801
|
.page-header {
|
|
@@ -15357,12 +15824,26 @@ a:hover { text-decoration: underline; }
|
|
|
15357
15824
|
.breadcrumb a:hover { color: var(--accent); }
|
|
15358
15825
|
.breadcrumb .sep { margin: 0 0.4rem; }
|
|
15359
15826
|
|
|
15827
|
+
/* Card groups */
|
|
15828
|
+
.card-group {
|
|
15829
|
+
margin-bottom: 1.5rem;
|
|
15830
|
+
}
|
|
15831
|
+
|
|
15832
|
+
.card-group-label {
|
|
15833
|
+
font-size: 0.7rem;
|
|
15834
|
+
text-transform: uppercase;
|
|
15835
|
+
letter-spacing: 0.08em;
|
|
15836
|
+
color: var(--text-dim);
|
|
15837
|
+
font-weight: 600;
|
|
15838
|
+
margin-bottom: 0.5rem;
|
|
15839
|
+
}
|
|
15840
|
+
|
|
15360
15841
|
/* Cards grid */
|
|
15361
15842
|
.cards {
|
|
15362
15843
|
display: grid;
|
|
15363
15844
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
15364
15845
|
gap: 1rem;
|
|
15365
|
-
margin-bottom:
|
|
15846
|
+
margin-bottom: 0.5rem;
|
|
15366
15847
|
}
|
|
15367
15848
|
|
|
15368
15849
|
.card {
|
|
@@ -15643,6 +16124,14 @@ tr:hover td {
|
|
|
15643
16124
|
font-family: var(--mono);
|
|
15644
16125
|
font-size: 0.85em;
|
|
15645
16126
|
}
|
|
16127
|
+
.detail-content hr {
|
|
16128
|
+
border: none;
|
|
16129
|
+
border-top: 1px solid var(--border);
|
|
16130
|
+
margin: 1.25rem 0;
|
|
16131
|
+
}
|
|
16132
|
+
.detail-content .table-wrap {
|
|
16133
|
+
margin: 0.75rem 0;
|
|
16134
|
+
}
|
|
15646
16135
|
|
|
15647
16136
|
/* Filters */
|
|
15648
16137
|
.filters {
|
|
@@ -15687,21 +16176,206 @@ tr:hover td {
|
|
|
15687
16176
|
.priority-high { color: var(--red); }
|
|
15688
16177
|
.priority-medium { color: var(--amber); }
|
|
15689
16178
|
.priority-low { color: var(--green); }
|
|
16179
|
+
|
|
16180
|
+
/* Health */
|
|
16181
|
+
.health-section-title {
|
|
16182
|
+
font-size: 1.1rem;
|
|
16183
|
+
font-weight: 600;
|
|
16184
|
+
margin: 2rem 0 1rem;
|
|
16185
|
+
color: var(--text);
|
|
16186
|
+
}
|
|
16187
|
+
|
|
16188
|
+
/* Mermaid diagrams */
|
|
16189
|
+
.mermaid-container {
|
|
16190
|
+
background: var(--bg-card);
|
|
16191
|
+
border: 1px solid var(--border);
|
|
16192
|
+
border-radius: var(--radius);
|
|
16193
|
+
padding: 1.5rem;
|
|
16194
|
+
margin: 1rem 0;
|
|
16195
|
+
overflow-x: auto;
|
|
16196
|
+
}
|
|
16197
|
+
|
|
16198
|
+
.mermaid-container .mermaid {
|
|
16199
|
+
display: flex;
|
|
16200
|
+
justify-content: center;
|
|
16201
|
+
}
|
|
16202
|
+
|
|
16203
|
+
.mermaid-empty {
|
|
16204
|
+
text-align: center;
|
|
16205
|
+
color: var(--text-dim);
|
|
16206
|
+
font-size: 0.875rem;
|
|
16207
|
+
}
|
|
16208
|
+
|
|
16209
|
+
.mermaid-row {
|
|
16210
|
+
display: grid;
|
|
16211
|
+
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
|
16212
|
+
gap: 1rem;
|
|
16213
|
+
}
|
|
16214
|
+
|
|
16215
|
+
.mermaid-row .mermaid-container {
|
|
16216
|
+
margin: 0;
|
|
16217
|
+
}
|
|
15690
16218
|
`;
|
|
15691
16219
|
}
|
|
15692
16220
|
|
|
16221
|
+
// src/web/templates/mermaid.ts
|
|
16222
|
+
function sanitize(text, maxLen = 40) {
|
|
16223
|
+
const cleaned = text.replace(/["'`]/g, "").replace(/[\r\n]+/g, " ");
|
|
16224
|
+
return cleaned.length > maxLen ? cleaned.slice(0, maxLen - 1) + "\u2026" : cleaned;
|
|
16225
|
+
}
|
|
16226
|
+
function mermaidBlock(definition) {
|
|
16227
|
+
return `<div class="mermaid-container"><pre class="mermaid">
|
|
16228
|
+
${definition}
|
|
16229
|
+
</pre></div>`;
|
|
16230
|
+
}
|
|
16231
|
+
function placeholder(message) {
|
|
16232
|
+
return `<div class="mermaid-container mermaid-empty"><p>${message}</p></div>`;
|
|
16233
|
+
}
|
|
16234
|
+
function buildTimelineGantt(data) {
|
|
16235
|
+
const sprintsWithDates = data.sprints.filter((s) => s.startDate && s.endDate);
|
|
16236
|
+
if (sprintsWithDates.length === 0) {
|
|
16237
|
+
return placeholder("No timeline data available \u2014 sprints need start and end dates.");
|
|
16238
|
+
}
|
|
16239
|
+
const epicMap = new Map(data.epics.map((e) => [e.id, e]));
|
|
16240
|
+
const lines = ["gantt", " title Project Timeline", " dateFormat YYYY-MM-DD"];
|
|
16241
|
+
for (const sprint of sprintsWithDates) {
|
|
16242
|
+
lines.push(` section ${sanitize(sprint.id + " " + sprint.title, 50)}`);
|
|
16243
|
+
const linked = sprint.linkedEpics.map((eid) => epicMap.get(eid)).filter(Boolean);
|
|
16244
|
+
if (linked.length === 0) {
|
|
16245
|
+
lines.push(` ${sanitize(sprint.title)} :${sprint.startDate}, ${sprint.endDate}`);
|
|
16246
|
+
} else {
|
|
16247
|
+
for (const epic of linked) {
|
|
16248
|
+
const tag = epic.status === "in-progress" ? "active, " : epic.status === "done" ? "done, " : "";
|
|
16249
|
+
lines.push(` ${sanitize(epic.id + " " + epic.title)} :${tag}${sprint.startDate}, ${sprint.endDate}`);
|
|
16250
|
+
}
|
|
16251
|
+
}
|
|
16252
|
+
}
|
|
16253
|
+
return mermaidBlock(lines.join("\n"));
|
|
16254
|
+
}
|
|
16255
|
+
function buildArtifactFlowchart(data) {
|
|
16256
|
+
if (data.features.length === 0 && data.epics.length === 0) {
|
|
16257
|
+
return placeholder("No artifact relationships found \u2014 create features and epics to see the hierarchy.");
|
|
16258
|
+
}
|
|
16259
|
+
const lines = ["graph TD"];
|
|
16260
|
+
lines.push(" classDef done fill:#065f46,stroke:#34d399,color:#d1fae5");
|
|
16261
|
+
lines.push(" classDef inprogress fill:#78350f,stroke:#fbbf24,color:#fef3c7");
|
|
16262
|
+
lines.push(" classDef blocked fill:#7f1d1d,stroke:#f87171,color:#fee2e2");
|
|
16263
|
+
lines.push(" classDef default fill:#1e293b,stroke:#475569,color:#e2e8f0");
|
|
16264
|
+
const nodeIds = /* @__PURE__ */ new Set();
|
|
16265
|
+
for (const epic of data.epics) {
|
|
16266
|
+
if (epic.linkedFeature) {
|
|
16267
|
+
const feature = data.features.find((f) => f.id === epic.linkedFeature);
|
|
16268
|
+
if (feature) {
|
|
16269
|
+
const fNode = feature.id.replace(/-/g, "_");
|
|
16270
|
+
const eNode = epic.id.replace(/-/g, "_");
|
|
16271
|
+
if (!nodeIds.has(fNode)) {
|
|
16272
|
+
lines.push(` ${fNode}["${sanitize(feature.id + " " + feature.title)}"]`);
|
|
16273
|
+
nodeIds.add(fNode);
|
|
16274
|
+
}
|
|
16275
|
+
if (!nodeIds.has(eNode)) {
|
|
16276
|
+
lines.push(` ${eNode}["${sanitize(epic.id + " " + epic.title)}"]`);
|
|
16277
|
+
nodeIds.add(eNode);
|
|
16278
|
+
}
|
|
16279
|
+
lines.push(` ${fNode} --> ${eNode}`);
|
|
16280
|
+
}
|
|
16281
|
+
}
|
|
16282
|
+
}
|
|
16283
|
+
for (const sprint of data.sprints) {
|
|
16284
|
+
const sNode = sprint.id.replace(/-/g, "_");
|
|
16285
|
+
for (const epicId of sprint.linkedEpics) {
|
|
16286
|
+
const epic = data.epics.find((e) => e.id === epicId);
|
|
16287
|
+
if (epic) {
|
|
16288
|
+
const eNode = epic.id.replace(/-/g, "_");
|
|
16289
|
+
if (!nodeIds.has(eNode)) {
|
|
16290
|
+
lines.push(` ${eNode}["${sanitize(epic.id + " " + epic.title)}"]`);
|
|
16291
|
+
nodeIds.add(eNode);
|
|
16292
|
+
}
|
|
16293
|
+
if (!nodeIds.has(sNode)) {
|
|
16294
|
+
lines.push(` ${sNode}["${sanitize(sprint.id + " " + sprint.title)}"]`);
|
|
16295
|
+
nodeIds.add(sNode);
|
|
16296
|
+
}
|
|
16297
|
+
lines.push(` ${eNode} --> ${sNode}`);
|
|
16298
|
+
}
|
|
16299
|
+
}
|
|
16300
|
+
}
|
|
16301
|
+
if (nodeIds.size === 0) {
|
|
16302
|
+
return placeholder("No artifact relationships found \u2014 link epics to features and sprints.");
|
|
16303
|
+
}
|
|
16304
|
+
const allItems = [
|
|
16305
|
+
...data.features.map((f) => ({ id: f.id, status: f.status })),
|
|
16306
|
+
...data.epics.map((e) => ({ id: e.id, status: e.status })),
|
|
16307
|
+
...data.sprints.map((s) => ({ id: s.id, status: s.status }))
|
|
16308
|
+
];
|
|
16309
|
+
for (const item of allItems) {
|
|
16310
|
+
const node = item.id.replace(/-/g, "_");
|
|
16311
|
+
if (!nodeIds.has(node)) continue;
|
|
16312
|
+
const cls = item.status === "done" || item.status === "completed" ? "done" : item.status === "in-progress" || item.status === "active" ? "inprogress" : item.status === "blocked" ? "blocked" : null;
|
|
16313
|
+
if (cls) {
|
|
16314
|
+
lines.push(` class ${node} ${cls}`);
|
|
16315
|
+
}
|
|
16316
|
+
}
|
|
16317
|
+
return mermaidBlock(lines.join("\n"));
|
|
16318
|
+
}
|
|
16319
|
+
function buildStatusPie(title, counts) {
|
|
16320
|
+
const entries = Object.entries(counts).filter(([, v]) => v > 0);
|
|
16321
|
+
if (entries.length === 0) {
|
|
16322
|
+
return placeholder(`No data for ${title}.`);
|
|
16323
|
+
}
|
|
16324
|
+
const lines = [`pie title ${sanitize(title, 60)}`];
|
|
16325
|
+
for (const [label, count] of entries) {
|
|
16326
|
+
lines.push(` "${sanitize(label, 30)}" : ${count}`);
|
|
16327
|
+
}
|
|
16328
|
+
return mermaidBlock(lines.join("\n"));
|
|
16329
|
+
}
|
|
16330
|
+
function buildHealthGauge(categories) {
|
|
16331
|
+
const valid = categories.filter((c) => c.total > 0);
|
|
16332
|
+
if (valid.length === 0) {
|
|
16333
|
+
return placeholder("No completeness data available.");
|
|
16334
|
+
}
|
|
16335
|
+
const pies = valid.map((cat) => {
|
|
16336
|
+
const incomplete = cat.total - cat.complete;
|
|
16337
|
+
const lines = [
|
|
16338
|
+
`pie title ${sanitize(cat.name, 30)}`,
|
|
16339
|
+
` "Complete" : ${cat.complete}`,
|
|
16340
|
+
` "Incomplete" : ${incomplete}`
|
|
16341
|
+
];
|
|
16342
|
+
return mermaidBlock(lines.join("\n"));
|
|
16343
|
+
});
|
|
16344
|
+
return `<div class="mermaid-row">${pies.join("\n")}</div>`;
|
|
16345
|
+
}
|
|
16346
|
+
|
|
15693
16347
|
// src/web/templates/pages/overview.ts
|
|
15694
|
-
function
|
|
15695
|
-
|
|
15696
|
-
(t) => `
|
|
16348
|
+
function renderCard(t) {
|
|
16349
|
+
return `
|
|
15697
16350
|
<div class="card">
|
|
15698
16351
|
<a href="/docs/${t.type}">
|
|
15699
16352
|
<div class="card-label">${escapeHtml(typeLabel(t.type))}s</div>
|
|
15700
16353
|
<div class="card-value">${t.total}</div>
|
|
15701
16354
|
${t.open > 0 ? `<div class="card-sub">${t.open} open</div>` : `<div class="card-sub">none open</div>`}
|
|
15702
16355
|
</a>
|
|
15703
|
-
</div
|
|
15704
|
-
|
|
16356
|
+
</div>`;
|
|
16357
|
+
}
|
|
16358
|
+
function overviewPage(data, diagrams, navGroups) {
|
|
16359
|
+
const typeMap = new Map(data.types.map((t) => [t.type, t]));
|
|
16360
|
+
const placed = /* @__PURE__ */ new Set();
|
|
16361
|
+
const groupSections = navGroups.map((group) => {
|
|
16362
|
+
const groupCards = group.types.filter((type) => typeMap.has(type)).map((type) => {
|
|
16363
|
+
placed.add(type);
|
|
16364
|
+
return renderCard(typeMap.get(type));
|
|
16365
|
+
});
|
|
16366
|
+
if (groupCards.length === 0) return "";
|
|
16367
|
+
return `
|
|
16368
|
+
<div class="card-group">
|
|
16369
|
+
<div class="card-group-label">${escapeHtml(group.label)}</div>
|
|
16370
|
+
<div class="cards">${groupCards.join("\n")}</div>
|
|
16371
|
+
</div>`;
|
|
16372
|
+
}).filter(Boolean).join("\n");
|
|
16373
|
+
const ungrouped = data.types.filter((t) => !placed.has(t.type));
|
|
16374
|
+
const ungroupedSection = ungrouped.length > 0 ? `
|
|
16375
|
+
<div class="card-group">
|
|
16376
|
+
<div class="card-group-label">Other</div>
|
|
16377
|
+
<div class="cards">${ungrouped.map(renderCard).join("\n")}</div>
|
|
16378
|
+
</div>` : "";
|
|
15705
16379
|
const rows = data.recent.map(
|
|
15706
16380
|
(doc) => `
|
|
15707
16381
|
<tr>
|
|
@@ -15717,9 +16391,14 @@ function overviewPage(data) {
|
|
|
15717
16391
|
<h2>Project Overview</h2>
|
|
15718
16392
|
</div>
|
|
15719
16393
|
|
|
15720
|
-
|
|
15721
|
-
|
|
15722
|
-
|
|
16394
|
+
${groupSections}
|
|
16395
|
+
${ungroupedSection}
|
|
16396
|
+
|
|
16397
|
+
<div class="section-title">Project Timeline</div>
|
|
16398
|
+
${buildTimelineGantt(diagrams)}
|
|
16399
|
+
|
|
16400
|
+
<div class="section-title">Artifact Relationships</div>
|
|
16401
|
+
${buildArtifactFlowchart(diagrams)}
|
|
15723
16402
|
|
|
15724
16403
|
<div class="section-title">Recent Activity</div>
|
|
15725
16404
|
${data.recent.length > 0 ? `
|
|
@@ -15886,6 +16565,76 @@ function garPage(report) {
|
|
|
15886
16565
|
<div class="gar-areas">
|
|
15887
16566
|
${areaCards}
|
|
15888
16567
|
</div>
|
|
16568
|
+
|
|
16569
|
+
<div class="section-title">Status Distribution</div>
|
|
16570
|
+
${buildStatusPie("Action Status", {
|
|
16571
|
+
Open: report.metrics.scope.open,
|
|
16572
|
+
Done: report.metrics.scope.done,
|
|
16573
|
+
"In Progress": Math.max(0, report.metrics.scope.total - report.metrics.scope.open - report.metrics.scope.done)
|
|
16574
|
+
})}
|
|
16575
|
+
`;
|
|
16576
|
+
}
|
|
16577
|
+
|
|
16578
|
+
// src/web/templates/pages/health.ts
|
|
16579
|
+
function healthPage(report, metrics) {
|
|
16580
|
+
const dotClass = `dot-${report.overall}`;
|
|
16581
|
+
function renderSection(title, categories) {
|
|
16582
|
+
const cards = categories.map(
|
|
16583
|
+
(cat) => `
|
|
16584
|
+
<div class="gar-area">
|
|
16585
|
+
<div class="area-header">
|
|
16586
|
+
<div class="area-dot dot-${cat.status}"></div>
|
|
16587
|
+
<div class="area-name">${escapeHtml(cat.name)}</div>
|
|
16588
|
+
</div>
|
|
16589
|
+
<div class="area-summary">${escapeHtml(cat.summary)}</div>
|
|
16590
|
+
${cat.items.length > 0 ? `<ul>${cat.items.map((item) => `<li><span class="ref-id">${escapeHtml(item.id)}</span>${escapeHtml(item.detail)}</li>`).join("")}</ul>` : ""}
|
|
16591
|
+
</div>`
|
|
16592
|
+
).join("\n");
|
|
16593
|
+
return `
|
|
16594
|
+
<div class="health-section-title">${escapeHtml(title)}</div>
|
|
16595
|
+
<div class="gar-areas">${cards}</div>
|
|
16596
|
+
`;
|
|
16597
|
+
}
|
|
16598
|
+
return `
|
|
16599
|
+
<div class="page-header">
|
|
16600
|
+
<h2>Governance Health Check</h2>
|
|
16601
|
+
<div class="subtitle">Generated ${escapeHtml(report.generatedAt)}</div>
|
|
16602
|
+
</div>
|
|
16603
|
+
|
|
16604
|
+
<div class="gar-overall">
|
|
16605
|
+
<div class="dot ${dotClass}"></div>
|
|
16606
|
+
<div class="label">Overall: ${escapeHtml(report.overall)}</div>
|
|
16607
|
+
</div>
|
|
16608
|
+
|
|
16609
|
+
${renderSection("Completeness", report.completeness)}
|
|
16610
|
+
|
|
16611
|
+
<div class="health-section-title">Completeness Overview</div>
|
|
16612
|
+
${buildHealthGauge(
|
|
16613
|
+
metrics ? Object.entries(metrics.completeness).map(([name, cat]) => ({
|
|
16614
|
+
name: name.replace(/\b\w/g, (c) => c.toUpperCase()),
|
|
16615
|
+
complete: cat.complete,
|
|
16616
|
+
total: cat.total
|
|
16617
|
+
})) : report.completeness.map((c) => {
|
|
16618
|
+
const match = c.summary.match(/(\d+)\s*\/\s*(\d+)/);
|
|
16619
|
+
return {
|
|
16620
|
+
name: c.name,
|
|
16621
|
+
complete: match ? parseInt(match[1], 10) : 0,
|
|
16622
|
+
total: match ? parseInt(match[2], 10) : 0
|
|
16623
|
+
};
|
|
16624
|
+
})
|
|
16625
|
+
)}
|
|
16626
|
+
|
|
16627
|
+
${renderSection("Process", report.process)}
|
|
16628
|
+
|
|
16629
|
+
<div class="health-section-title">Process Summary</div>
|
|
16630
|
+
${metrics ? buildStatusPie("Process Health", {
|
|
16631
|
+
Stale: metrics.process.stale.length,
|
|
16632
|
+
"Aging Actions": metrics.process.agingActions.length,
|
|
16633
|
+
Healthy: Math.max(
|
|
16634
|
+
0,
|
|
16635
|
+
(metrics.completeness ? Object.values(metrics.completeness).reduce((sum, c) => sum + c.total, 0) : 0) - metrics.process.stale.length - metrics.process.agingActions.length
|
|
16636
|
+
)
|
|
16637
|
+
}) : ""}
|
|
15889
16638
|
`;
|
|
15890
16639
|
}
|
|
15891
16640
|
|
|
@@ -15952,7 +16701,8 @@ function handleRequest(req, res, store, projectName, navGroups) {
|
|
|
15952
16701
|
}
|
|
15953
16702
|
if (pathname === "/") {
|
|
15954
16703
|
const data = getOverviewData(store);
|
|
15955
|
-
const
|
|
16704
|
+
const diagrams = getDiagramData(store);
|
|
16705
|
+
const body = overviewPage(data, diagrams, navGroups);
|
|
15956
16706
|
respond(res, layout({ title: "Overview", activePath: "/", projectName, navGroups }, body));
|
|
15957
16707
|
return;
|
|
15958
16708
|
}
|
|
@@ -15962,6 +16712,13 @@ function handleRequest(req, res, store, projectName, navGroups) {
|
|
|
15962
16712
|
respond(res, layout({ title: "GAR Report", activePath: "/gar", projectName, navGroups }, body));
|
|
15963
16713
|
return;
|
|
15964
16714
|
}
|
|
16715
|
+
if (pathname === "/health") {
|
|
16716
|
+
const healthMetrics = collectHealthMetrics(store);
|
|
16717
|
+
const report = evaluateHealth(projectName, healthMetrics);
|
|
16718
|
+
const body = healthPage(report, healthMetrics);
|
|
16719
|
+
respond(res, layout({ title: "Health Check", activePath: "/health", projectName, navGroups }, body));
|
|
16720
|
+
return;
|
|
16721
|
+
}
|
|
15965
16722
|
const boardMatch = pathname.match(/^\/board(?:\/([^/]+))?$/);
|
|
15966
16723
|
if (boardMatch) {
|
|
15967
16724
|
const type = boardMatch[1];
|
|
@@ -16042,7 +16799,7 @@ function createMeetingTools(store) {
|
|
|
16042
16799
|
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
16043
16800
|
};
|
|
16044
16801
|
},
|
|
16045
|
-
{ annotations: {
|
|
16802
|
+
{ annotations: { readOnlyHint: true } }
|
|
16046
16803
|
),
|
|
16047
16804
|
tool7(
|
|
16048
16805
|
"get_meeting",
|
|
@@ -16069,7 +16826,7 @@ function createMeetingTools(store) {
|
|
|
16069
16826
|
]
|
|
16070
16827
|
};
|
|
16071
16828
|
},
|
|
16072
|
-
{ annotations: {
|
|
16829
|
+
{ annotations: { readOnlyHint: true } }
|
|
16073
16830
|
),
|
|
16074
16831
|
tool7(
|
|
16075
16832
|
"create_meeting",
|
|
@@ -16194,7 +16951,7 @@ function createMeetingTools(store) {
|
|
|
16194
16951
|
content: [{ type: "text", text: sections.join("\n") }]
|
|
16195
16952
|
};
|
|
16196
16953
|
},
|
|
16197
|
-
{ annotations: {
|
|
16954
|
+
{ annotations: { readOnlyHint: true } }
|
|
16198
16955
|
)
|
|
16199
16956
|
];
|
|
16200
16957
|
}
|
|
@@ -16219,7 +16976,8 @@ function createReportTools(store) {
|
|
|
16219
16976
|
id: d.frontmatter.id,
|
|
16220
16977
|
title: d.frontmatter.title,
|
|
16221
16978
|
owner: d.frontmatter.owner,
|
|
16222
|
-
priority: d.frontmatter.priority
|
|
16979
|
+
priority: d.frontmatter.priority,
|
|
16980
|
+
dueDate: d.frontmatter.dueDate
|
|
16223
16981
|
})),
|
|
16224
16982
|
completedActions: completedActions.map((d) => ({
|
|
16225
16983
|
id: d.frontmatter.id,
|
|
@@ -16238,7 +16996,7 @@ function createReportTools(store) {
|
|
|
16238
16996
|
content: [{ type: "text", text: JSON.stringify(report, null, 2) }]
|
|
16239
16997
|
};
|
|
16240
16998
|
},
|
|
16241
|
-
{ annotations: {
|
|
16999
|
+
{ annotations: { readOnlyHint: true } }
|
|
16242
17000
|
),
|
|
16243
17001
|
tool8(
|
|
16244
17002
|
"generate_risk_register",
|
|
@@ -16247,7 +17005,7 @@ function createReportTools(store) {
|
|
|
16247
17005
|
async () => {
|
|
16248
17006
|
const allDocs = store.list();
|
|
16249
17007
|
const taggedRisks = allDocs.filter(
|
|
16250
|
-
(d) => d.frontmatter.tags?.includes("risk")
|
|
17008
|
+
(d) => d.frontmatter.tags?.includes("risk") && d.frontmatter.status !== "done" && d.frontmatter.status !== "closed"
|
|
16251
17009
|
);
|
|
16252
17010
|
const highPriorityActions = store.list({ type: "action", status: "open" }).filter((d) => d.frontmatter.priority === "high");
|
|
16253
17011
|
const unresolvedQuestions = store.list({ type: "question", status: "open" });
|
|
@@ -16282,7 +17040,7 @@ function createReportTools(store) {
|
|
|
16282
17040
|
content: [{ type: "text", text: JSON.stringify(register, null, 2) }]
|
|
16283
17041
|
};
|
|
16284
17042
|
},
|
|
16285
|
-
{ annotations: {
|
|
17043
|
+
{ annotations: { readOnlyHint: true } }
|
|
16286
17044
|
),
|
|
16287
17045
|
tool8(
|
|
16288
17046
|
"generate_gar_report",
|
|
@@ -16295,7 +17053,7 @@ function createReportTools(store) {
|
|
|
16295
17053
|
content: [{ type: "text", text: JSON.stringify(report, null, 2) }]
|
|
16296
17054
|
};
|
|
16297
17055
|
},
|
|
16298
|
-
{ annotations: {
|
|
17056
|
+
{ annotations: { readOnlyHint: true } }
|
|
16299
17057
|
),
|
|
16300
17058
|
tool8(
|
|
16301
17059
|
"generate_epic_progress",
|
|
@@ -16380,7 +17138,7 @@ function createReportTools(store) {
|
|
|
16380
17138
|
]
|
|
16381
17139
|
};
|
|
16382
17140
|
},
|
|
16383
|
-
{ annotations: {
|
|
17141
|
+
{ annotations: { readOnlyHint: true } }
|
|
16384
17142
|
),
|
|
16385
17143
|
tool8(
|
|
16386
17144
|
"generate_sprint_progress",
|
|
@@ -16425,7 +17183,8 @@ function createReportTools(store) {
|
|
|
16425
17183
|
id: d.frontmatter.id,
|
|
16426
17184
|
title: d.frontmatter.title,
|
|
16427
17185
|
type: d.frontmatter.type,
|
|
16428
|
-
status: d.frontmatter.status
|
|
17186
|
+
status: d.frontmatter.status,
|
|
17187
|
+
dueDate: d.frontmatter.dueDate
|
|
16429
17188
|
}))
|
|
16430
17189
|
}
|
|
16431
17190
|
};
|
|
@@ -16434,7 +17193,7 @@ function createReportTools(store) {
|
|
|
16434
17193
|
content: [{ type: "text", text: JSON.stringify({ sprints }, null, 2) }]
|
|
16435
17194
|
};
|
|
16436
17195
|
},
|
|
16437
|
-
{ annotations: {
|
|
17196
|
+
{ annotations: { readOnlyHint: true } }
|
|
16438
17197
|
),
|
|
16439
17198
|
tool8(
|
|
16440
17199
|
"generate_feature_progress",
|
|
@@ -16474,7 +17233,20 @@ function createReportTools(store) {
|
|
|
16474
17233
|
content: [{ type: "text", text: JSON.stringify({ features }, null, 2) }]
|
|
16475
17234
|
};
|
|
16476
17235
|
},
|
|
16477
|
-
{ annotations: {
|
|
17236
|
+
{ annotations: { readOnlyHint: true } }
|
|
17237
|
+
),
|
|
17238
|
+
tool8(
|
|
17239
|
+
"generate_health_report",
|
|
17240
|
+
"Generate a governance health check report covering artifact completeness and process health metrics",
|
|
17241
|
+
{},
|
|
17242
|
+
async () => {
|
|
17243
|
+
const metrics = collectHealthMetrics(store);
|
|
17244
|
+
const report = evaluateHealth("project", metrics);
|
|
17245
|
+
return {
|
|
17246
|
+
content: [{ type: "text", text: JSON.stringify(report, null, 2) }]
|
|
17247
|
+
};
|
|
17248
|
+
},
|
|
17249
|
+
{ annotations: { readOnlyHint: true } }
|
|
16478
17250
|
),
|
|
16479
17251
|
tool8(
|
|
16480
17252
|
"save_report",
|
|
@@ -16536,7 +17308,7 @@ function createFeatureTools(store) {
|
|
|
16536
17308
|
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
16537
17309
|
};
|
|
16538
17310
|
},
|
|
16539
|
-
{ annotations: {
|
|
17311
|
+
{ annotations: { readOnlyHint: true } }
|
|
16540
17312
|
),
|
|
16541
17313
|
tool9(
|
|
16542
17314
|
"get_feature",
|
|
@@ -16563,7 +17335,7 @@ function createFeatureTools(store) {
|
|
|
16563
17335
|
]
|
|
16564
17336
|
};
|
|
16565
17337
|
},
|
|
16566
|
-
{ annotations: {
|
|
17338
|
+
{ annotations: { readOnlyHint: true } }
|
|
16567
17339
|
),
|
|
16568
17340
|
tool9(
|
|
16569
17341
|
"create_feature",
|
|
@@ -16604,7 +17376,8 @@ function createFeatureTools(store) {
|
|
|
16604
17376
|
status: external_exports.enum(["draft", "approved", "deferred", "done"]).optional().describe("New status"),
|
|
16605
17377
|
content: external_exports.string().optional().describe("New content"),
|
|
16606
17378
|
owner: external_exports.string().optional().describe("New owner"),
|
|
16607
|
-
priority: external_exports.enum(["critical", "high", "medium", "low"]).optional().describe("New priority")
|
|
17379
|
+
priority: external_exports.enum(["critical", "high", "medium", "low"]).optional().describe("New priority"),
|
|
17380
|
+
tags: external_exports.array(external_exports.string()).optional().describe("Replace tags (e.g. remove 'risk', add 'risk-mitigated')")
|
|
16608
17381
|
},
|
|
16609
17382
|
async (args) => {
|
|
16610
17383
|
const { id, content, ...updates } = args;
|
|
@@ -16654,7 +17427,7 @@ function createEpicTools(store) {
|
|
|
16654
17427
|
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
16655
17428
|
};
|
|
16656
17429
|
},
|
|
16657
|
-
{ annotations: {
|
|
17430
|
+
{ annotations: { readOnlyHint: true } }
|
|
16658
17431
|
),
|
|
16659
17432
|
tool10(
|
|
16660
17433
|
"get_epic",
|
|
@@ -16681,7 +17454,7 @@ function createEpicTools(store) {
|
|
|
16681
17454
|
]
|
|
16682
17455
|
};
|
|
16683
17456
|
},
|
|
16684
|
-
{ annotations: {
|
|
17457
|
+
{ annotations: { readOnlyHint: true } }
|
|
16685
17458
|
),
|
|
16686
17459
|
tool10(
|
|
16687
17460
|
"create_epic",
|
|
@@ -16761,7 +17534,8 @@ function createEpicTools(store) {
|
|
|
16761
17534
|
content: external_exports.string().optional().describe("New content"),
|
|
16762
17535
|
owner: external_exports.string().optional().describe("New owner"),
|
|
16763
17536
|
targetDate: external_exports.string().optional().describe("New target date"),
|
|
16764
|
-
estimatedEffort: external_exports.string().optional().describe("New estimated effort")
|
|
17537
|
+
estimatedEffort: external_exports.string().optional().describe("New estimated effort"),
|
|
17538
|
+
tags: external_exports.array(external_exports.string()).optional().describe("Replace tags (e.g. remove 'risk', add 'risk-mitigated')")
|
|
16765
17539
|
},
|
|
16766
17540
|
async (args) => {
|
|
16767
17541
|
const { id, content, ...updates } = args;
|
|
@@ -16813,7 +17587,7 @@ function createContributionTools(store) {
|
|
|
16813
17587
|
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
16814
17588
|
};
|
|
16815
17589
|
},
|
|
16816
|
-
{ annotations: {
|
|
17590
|
+
{ annotations: { readOnlyHint: true } }
|
|
16817
17591
|
),
|
|
16818
17592
|
tool11(
|
|
16819
17593
|
"get_contribution",
|
|
@@ -16840,7 +17614,7 @@ function createContributionTools(store) {
|
|
|
16840
17614
|
]
|
|
16841
17615
|
};
|
|
16842
17616
|
},
|
|
16843
|
-
{ annotations: {
|
|
17617
|
+
{ annotations: { readOnlyHint: true } }
|
|
16844
17618
|
),
|
|
16845
17619
|
tool11(
|
|
16846
17620
|
"create_contribution",
|
|
@@ -16925,7 +17699,7 @@ function createSprintTools(store) {
|
|
|
16925
17699
|
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
16926
17700
|
};
|
|
16927
17701
|
},
|
|
16928
|
-
{ annotations: {
|
|
17702
|
+
{ annotations: { readOnlyHint: true } }
|
|
16929
17703
|
),
|
|
16930
17704
|
tool12(
|
|
16931
17705
|
"get_sprint",
|
|
@@ -16952,7 +17726,7 @@ function createSprintTools(store) {
|
|
|
16952
17726
|
]
|
|
16953
17727
|
};
|
|
16954
17728
|
},
|
|
16955
|
-
{ annotations: {
|
|
17729
|
+
{ annotations: { readOnlyHint: true } }
|
|
16956
17730
|
),
|
|
16957
17731
|
tool12(
|
|
16958
17732
|
"create_sprint",
|
|
@@ -17249,7 +18023,7 @@ function createSprintPlanningTools(store) {
|
|
|
17249
18023
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
17250
18024
|
};
|
|
17251
18025
|
},
|
|
17252
|
-
{ annotations: {
|
|
18026
|
+
{ annotations: { readOnlyHint: true } }
|
|
17253
18027
|
)
|
|
17254
18028
|
];
|
|
17255
18029
|
}
|
|
@@ -17303,6 +18077,7 @@ var genericAgilePlugin = {
|
|
|
17303
18077
|
- Do NOT create epics \u2014 that is the Tech Lead's responsibility. You can view epics to track progress.
|
|
17304
18078
|
- Use priority levels (critical, high, medium, low) to communicate business value.
|
|
17305
18079
|
- Tag features for categorization and cross-referencing.
|
|
18080
|
+
- Include a \`dueDate\` on actions when target dates are known, to enable schedule tracking and overdue detection.
|
|
17306
18081
|
|
|
17307
18082
|
**Contribution Tools:**
|
|
17308
18083
|
- **list_contributions** / **get_contribution**: Browse and read contribution records.
|
|
@@ -17333,6 +18108,7 @@ var genericAgilePlugin = {
|
|
|
17333
18108
|
- Tag work items (actions, decisions, questions) with \`epic:E-xxx\` to group them under an epic.
|
|
17334
18109
|
- Collaborate with the Delivery Manager on target dates and effort estimates.
|
|
17335
18110
|
- Each epic should have a clear scope and definition of done.
|
|
18111
|
+
- Set \`dueDate\` on technical actions based on sprint timelines or epic target dates. Use the \`sprints\` parameter to assign actions to relevant sprints.
|
|
17336
18112
|
|
|
17337
18113
|
**Contribution Tools:**
|
|
17338
18114
|
- **list_contributions** / **get_contribution**: Browse and read contribution records.
|
|
@@ -17392,6 +18168,11 @@ var genericAgilePlugin = {
|
|
|
17392
18168
|
- **generate_sprint_progress**: Progress report for a specific sprint or all sprints \u2014 shows linked epics with statuses, work items tagged \`sprint:SP-xxx\` grouped by status, and done/total completion %.
|
|
17393
18169
|
- Use \`save_report\` with reportType "sprint-progress" to persist sprint reports.
|
|
17394
18170
|
|
|
18171
|
+
**Date Enforcement:**
|
|
18172
|
+
- Always set \`dueDate\` when creating or updating actions. Use the \`sprints\` parameter to assign actions to sprints \u2014 the tool translates this into \`sprint:SP-xxx\` tags automatically.
|
|
18173
|
+
- When create_action suggests matching sprints in its response, review and assign accordingly using update_action.
|
|
18174
|
+
- Use \`suggest_sprints_for_action\` to find the right sprint for existing actions that lack sprint assignment.
|
|
18175
|
+
|
|
17395
18176
|
**Sprint Workflow:**
|
|
17396
18177
|
- Create sprints with clear goals and date boundaries.
|
|
17397
18178
|
- Assign epics to sprints via linkedEpics.
|
|
@@ -17411,7 +18192,7 @@ var genericAgilePlugin = {
|
|
|
17411
18192
|
**Sprints** (SP-xxx): Time-boxed iterations that group epics and work items with delivery dates. Sprints progress through planned \u2192 active \u2192 completed (or cancelled).
|
|
17412
18193
|
**Meetings**: Meeting records with attendees, agendas, and notes.
|
|
17413
18194
|
|
|
17414
|
-
**Key workflow rule:** Epics must link to approved features \u2014 the system enforces this. The Product Owner defines and approves features, the Tech Lead breaks them into epics, the Delivery Manager plans sprints and tracks dates and progress. Work items are associated with sprints via \`sprint:SP-xxx\` tags.
|
|
18195
|
+
**Key workflow rule:** Epics must link to approved features \u2014 the system enforces this. The Product Owner defines and approves features, the Tech Lead breaks them into epics, the Delivery Manager plans sprints and tracks dates and progress. Work items are associated with sprints via \`sprint:SP-xxx\` tags. Actions support a \`dueDate\` field for schedule tracking \u2014 actions with a past due date are automatically flagged as overdue in GAR reports. Use the \`sprints\` parameter on create_action/update_action to assign actions to sprints.
|
|
17415
18196
|
|
|
17416
18197
|
- **list_meetings** / **get_meeting**: Browse and read meeting records.
|
|
17417
18198
|
- **create_meeting**: Record meetings with attendees, date, and agenda. The meeting date is required \u2014 extract it from the meeting content or ask the user if not found.
|
|
@@ -17457,7 +18238,7 @@ function createUseCaseTools(store) {
|
|
|
17457
18238
|
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
17458
18239
|
};
|
|
17459
18240
|
},
|
|
17460
|
-
{ annotations: {
|
|
18241
|
+
{ annotations: { readOnlyHint: true } }
|
|
17461
18242
|
),
|
|
17462
18243
|
tool14(
|
|
17463
18244
|
"get_use_case",
|
|
@@ -17484,7 +18265,7 @@ function createUseCaseTools(store) {
|
|
|
17484
18265
|
]
|
|
17485
18266
|
};
|
|
17486
18267
|
},
|
|
17487
|
-
{ annotations: {
|
|
18268
|
+
{ annotations: { readOnlyHint: true } }
|
|
17488
18269
|
),
|
|
17489
18270
|
tool14(
|
|
17490
18271
|
"create_use_case",
|
|
@@ -17582,7 +18363,7 @@ function createTechAssessmentTools(store) {
|
|
|
17582
18363
|
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
17583
18364
|
};
|
|
17584
18365
|
},
|
|
17585
|
-
{ annotations: {
|
|
18366
|
+
{ annotations: { readOnlyHint: true } }
|
|
17586
18367
|
),
|
|
17587
18368
|
tool15(
|
|
17588
18369
|
"get_tech_assessment",
|
|
@@ -17609,7 +18390,7 @@ function createTechAssessmentTools(store) {
|
|
|
17609
18390
|
]
|
|
17610
18391
|
};
|
|
17611
18392
|
},
|
|
17612
|
-
{ annotations: {
|
|
18393
|
+
{ annotations: { readOnlyHint: true } }
|
|
17613
18394
|
),
|
|
17614
18395
|
tool15(
|
|
17615
18396
|
"create_tech_assessment",
|
|
@@ -17743,7 +18524,7 @@ function createExtensionDesignTools(store) {
|
|
|
17743
18524
|
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
17744
18525
|
};
|
|
17745
18526
|
},
|
|
17746
|
-
{ annotations: {
|
|
18527
|
+
{ annotations: { readOnlyHint: true } }
|
|
17747
18528
|
),
|
|
17748
18529
|
tool16(
|
|
17749
18530
|
"get_extension_design",
|
|
@@ -17770,7 +18551,7 @@ function createExtensionDesignTools(store) {
|
|
|
17770
18551
|
]
|
|
17771
18552
|
};
|
|
17772
18553
|
},
|
|
17773
|
-
{ annotations: {
|
|
18554
|
+
{ annotations: { readOnlyHint: true } }
|
|
17774
18555
|
),
|
|
17775
18556
|
tool16(
|
|
17776
18557
|
"create_extension_design",
|
|
@@ -17922,7 +18703,7 @@ function createAemReportTools(store) {
|
|
|
17922
18703
|
]
|
|
17923
18704
|
};
|
|
17924
18705
|
},
|
|
17925
|
-
{ annotations: {
|
|
18706
|
+
{ annotations: { readOnlyHint: true } }
|
|
17926
18707
|
),
|
|
17927
18708
|
tool17(
|
|
17928
18709
|
"generate_tech_readiness",
|
|
@@ -17974,7 +18755,7 @@ function createAemReportTools(store) {
|
|
|
17974
18755
|
]
|
|
17975
18756
|
};
|
|
17976
18757
|
},
|
|
17977
|
-
{ annotations: {
|
|
18758
|
+
{ annotations: { readOnlyHint: true } }
|
|
17978
18759
|
),
|
|
17979
18760
|
tool17(
|
|
17980
18761
|
"generate_phase_status",
|
|
@@ -18029,14 +18810,14 @@ function createAemReportTools(store) {
|
|
|
18029
18810
|
]
|
|
18030
18811
|
};
|
|
18031
18812
|
},
|
|
18032
|
-
{ annotations: {
|
|
18813
|
+
{ annotations: { readOnlyHint: true } }
|
|
18033
18814
|
)
|
|
18034
18815
|
];
|
|
18035
18816
|
}
|
|
18036
18817
|
|
|
18037
18818
|
// src/plugins/builtin/tools/aem-phase.ts
|
|
18038
|
-
import * as
|
|
18039
|
-
import * as
|
|
18819
|
+
import * as fs5 from "fs";
|
|
18820
|
+
import * as path5 from "path";
|
|
18040
18821
|
import * as YAML2 from "yaml";
|
|
18041
18822
|
import { tool as tool18 } from "@anthropic-ai/claude-agent-sdk";
|
|
18042
18823
|
var PHASES = ["assess-use-case", "assess-technology", "define-solution"];
|
|
@@ -18061,7 +18842,7 @@ function createAemPhaseTools(store, marvinDir) {
|
|
|
18061
18842
|
]
|
|
18062
18843
|
};
|
|
18063
18844
|
},
|
|
18064
|
-
{ annotations: {
|
|
18845
|
+
{ annotations: { readOnlyHint: true } }
|
|
18065
18846
|
),
|
|
18066
18847
|
tool18(
|
|
18067
18848
|
"advance_phase",
|
|
@@ -18141,8 +18922,8 @@ function createAemPhaseTools(store, marvinDir) {
|
|
|
18141
18922
|
function readPhase(marvinDir) {
|
|
18142
18923
|
if (!marvinDir) return void 0;
|
|
18143
18924
|
try {
|
|
18144
|
-
const configPath =
|
|
18145
|
-
const raw =
|
|
18925
|
+
const configPath = path5.join(marvinDir, "config.yaml");
|
|
18926
|
+
const raw = fs5.readFileSync(configPath, "utf-8");
|
|
18146
18927
|
const config2 = YAML2.parse(raw);
|
|
18147
18928
|
const aem = config2.aem;
|
|
18148
18929
|
return aem?.currentPhase;
|
|
@@ -18152,14 +18933,14 @@ function readPhase(marvinDir) {
|
|
|
18152
18933
|
}
|
|
18153
18934
|
function writePhase(marvinDir, phase) {
|
|
18154
18935
|
if (!marvinDir) return;
|
|
18155
|
-
const configPath =
|
|
18156
|
-
const raw =
|
|
18936
|
+
const configPath = path5.join(marvinDir, "config.yaml");
|
|
18937
|
+
const raw = fs5.readFileSync(configPath, "utf-8");
|
|
18157
18938
|
const config2 = YAML2.parse(raw);
|
|
18158
18939
|
if (!config2.aem) {
|
|
18159
18940
|
config2.aem = {};
|
|
18160
18941
|
}
|
|
18161
18942
|
config2.aem.currentPhase = phase;
|
|
18162
|
-
|
|
18943
|
+
fs5.writeFileSync(configPath, YAML2.stringify(config2), "utf-8");
|
|
18163
18944
|
}
|
|
18164
18945
|
function getPhaseDescription(phase) {
|
|
18165
18946
|
switch (phase) {
|
|
@@ -18327,8 +19108,8 @@ function getPluginPromptFragment(plugin, personaId) {
|
|
|
18327
19108
|
}
|
|
18328
19109
|
|
|
18329
19110
|
// src/skills/registry.ts
|
|
18330
|
-
import * as
|
|
18331
|
-
import * as
|
|
19111
|
+
import * as fs6 from "fs";
|
|
19112
|
+
import * as path6 from "path";
|
|
18332
19113
|
import { fileURLToPath } from "url";
|
|
18333
19114
|
import * as YAML3 from "yaml";
|
|
18334
19115
|
import matter2 from "gray-matter";
|
|
@@ -18381,8 +19162,8 @@ var JiraClient = class {
|
|
|
18381
19162
|
this.baseUrl = `https://${config2.host}/rest/api/2`;
|
|
18382
19163
|
this.authHeader = "Basic " + Buffer.from(`${config2.email}:${config2.apiToken}`).toString("base64");
|
|
18383
19164
|
}
|
|
18384
|
-
async request(
|
|
18385
|
-
const url2 = `${this.baseUrl}${
|
|
19165
|
+
async request(path20, method = "GET", body) {
|
|
19166
|
+
const url2 = `${this.baseUrl}${path20}`;
|
|
18386
19167
|
const headers = {
|
|
18387
19168
|
Authorization: this.authHeader,
|
|
18388
19169
|
"Content-Type": "application/json",
|
|
@@ -18396,7 +19177,7 @@ var JiraClient = class {
|
|
|
18396
19177
|
if (!response.ok) {
|
|
18397
19178
|
const text = await response.text().catch(() => "");
|
|
18398
19179
|
throw new Error(
|
|
18399
|
-
`Jira API error ${response.status} ${method} ${
|
|
19180
|
+
`Jira API error ${response.status} ${method} ${path20}: ${text}`
|
|
18400
19181
|
);
|
|
18401
19182
|
}
|
|
18402
19183
|
if (response.status === 204) return void 0;
|
|
@@ -18506,7 +19287,7 @@ function createJiraTools(store) {
|
|
|
18506
19287
|
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
18507
19288
|
};
|
|
18508
19289
|
},
|
|
18509
|
-
{ annotations: {
|
|
19290
|
+
{ annotations: { readOnlyHint: true } }
|
|
18510
19291
|
),
|
|
18511
19292
|
tool19(
|
|
18512
19293
|
"get_jira_issue",
|
|
@@ -18538,7 +19319,7 @@ function createJiraTools(store) {
|
|
|
18538
19319
|
]
|
|
18539
19320
|
};
|
|
18540
19321
|
},
|
|
18541
|
-
{ annotations: {
|
|
19322
|
+
{ annotations: { readOnlyHint: true } }
|
|
18542
19323
|
),
|
|
18543
19324
|
// --- Jira → Local tools ---
|
|
18544
19325
|
tool19(
|
|
@@ -18876,13 +19657,13 @@ var GOVERNANCE_TOOL_NAMES = [
|
|
|
18876
19657
|
];
|
|
18877
19658
|
function getBuiltinSkillsDir() {
|
|
18878
19659
|
const thisFile = fileURLToPath(import.meta.url);
|
|
18879
|
-
return
|
|
19660
|
+
return path6.join(path6.dirname(thisFile), "builtin");
|
|
18880
19661
|
}
|
|
18881
19662
|
function loadSkillFromDirectory(dirPath) {
|
|
18882
|
-
const skillMdPath =
|
|
18883
|
-
if (!
|
|
19663
|
+
const skillMdPath = path6.join(dirPath, "SKILL.md");
|
|
19664
|
+
if (!fs6.existsSync(skillMdPath)) return void 0;
|
|
18884
19665
|
try {
|
|
18885
|
-
const raw =
|
|
19666
|
+
const raw = fs6.readFileSync(skillMdPath, "utf-8");
|
|
18886
19667
|
const { data, content } = matter2(raw);
|
|
18887
19668
|
if (!data.name || !data.description) return void 0;
|
|
18888
19669
|
const metadata = data.metadata ?? {};
|
|
@@ -18893,13 +19674,13 @@ function loadSkillFromDirectory(dirPath) {
|
|
|
18893
19674
|
if (wildcardPrompt) {
|
|
18894
19675
|
promptFragments["*"] = wildcardPrompt;
|
|
18895
19676
|
}
|
|
18896
|
-
const personasDir =
|
|
18897
|
-
if (
|
|
19677
|
+
const personasDir = path6.join(dirPath, "personas");
|
|
19678
|
+
if (fs6.existsSync(personasDir)) {
|
|
18898
19679
|
try {
|
|
18899
|
-
for (const file2 of
|
|
19680
|
+
for (const file2 of fs6.readdirSync(personasDir)) {
|
|
18900
19681
|
if (!file2.endsWith(".md")) continue;
|
|
18901
19682
|
const personaId = file2.replace(/\.md$/, "");
|
|
18902
|
-
const personaPrompt =
|
|
19683
|
+
const personaPrompt = fs6.readFileSync(path6.join(personasDir, file2), "utf-8").trim();
|
|
18903
19684
|
if (personaPrompt) {
|
|
18904
19685
|
promptFragments[personaId] = personaPrompt;
|
|
18905
19686
|
}
|
|
@@ -18908,10 +19689,10 @@ function loadSkillFromDirectory(dirPath) {
|
|
|
18908
19689
|
}
|
|
18909
19690
|
}
|
|
18910
19691
|
let actions;
|
|
18911
|
-
const actionsPath =
|
|
18912
|
-
if (
|
|
19692
|
+
const actionsPath = path6.join(dirPath, "actions.yaml");
|
|
19693
|
+
if (fs6.existsSync(actionsPath)) {
|
|
18913
19694
|
try {
|
|
18914
|
-
const actionsRaw =
|
|
19695
|
+
const actionsRaw = fs6.readFileSync(actionsPath, "utf-8");
|
|
18915
19696
|
actions = YAML3.parse(actionsRaw);
|
|
18916
19697
|
} catch {
|
|
18917
19698
|
}
|
|
@@ -18938,10 +19719,10 @@ function loadAllSkills(marvinDir) {
|
|
|
18938
19719
|
}
|
|
18939
19720
|
try {
|
|
18940
19721
|
const builtinDir = getBuiltinSkillsDir();
|
|
18941
|
-
if (
|
|
18942
|
-
for (const entry of
|
|
18943
|
-
const entryPath =
|
|
18944
|
-
if (!
|
|
19722
|
+
if (fs6.existsSync(builtinDir)) {
|
|
19723
|
+
for (const entry of fs6.readdirSync(builtinDir)) {
|
|
19724
|
+
const entryPath = path6.join(builtinDir, entry);
|
|
19725
|
+
if (!fs6.statSync(entryPath).isDirectory()) continue;
|
|
18945
19726
|
if (skills.has(entry)) continue;
|
|
18946
19727
|
const skill = loadSkillFromDirectory(entryPath);
|
|
18947
19728
|
if (skill) skills.set(skill.id, skill);
|
|
@@ -18950,18 +19731,18 @@ function loadAllSkills(marvinDir) {
|
|
|
18950
19731
|
} catch {
|
|
18951
19732
|
}
|
|
18952
19733
|
if (marvinDir) {
|
|
18953
|
-
const skillsDir =
|
|
18954
|
-
if (
|
|
19734
|
+
const skillsDir = path6.join(marvinDir, "skills");
|
|
19735
|
+
if (fs6.existsSync(skillsDir)) {
|
|
18955
19736
|
let entries;
|
|
18956
19737
|
try {
|
|
18957
|
-
entries =
|
|
19738
|
+
entries = fs6.readdirSync(skillsDir);
|
|
18958
19739
|
} catch {
|
|
18959
19740
|
entries = [];
|
|
18960
19741
|
}
|
|
18961
19742
|
for (const entry of entries) {
|
|
18962
|
-
const entryPath =
|
|
19743
|
+
const entryPath = path6.join(skillsDir, entry);
|
|
18963
19744
|
try {
|
|
18964
|
-
if (
|
|
19745
|
+
if (fs6.statSync(entryPath).isDirectory()) {
|
|
18965
19746
|
const skill = loadSkillFromDirectory(entryPath);
|
|
18966
19747
|
if (skill) skills.set(skill.id, skill);
|
|
18967
19748
|
continue;
|
|
@@ -18971,7 +19752,7 @@ function loadAllSkills(marvinDir) {
|
|
|
18971
19752
|
}
|
|
18972
19753
|
if (!entry.endsWith(".yaml") && !entry.endsWith(".yml")) continue;
|
|
18973
19754
|
try {
|
|
18974
|
-
const raw =
|
|
19755
|
+
const raw = fs6.readFileSync(entryPath, "utf-8");
|
|
18975
19756
|
const parsed = YAML3.parse(raw);
|
|
18976
19757
|
if (!parsed?.id || !parsed?.name || !parsed?.version) continue;
|
|
18977
19758
|
const skill = {
|
|
@@ -19076,12 +19857,12 @@ function getSkillAgentDefinitions(skillIds, allSkills) {
|
|
|
19076
19857
|
return agents;
|
|
19077
19858
|
}
|
|
19078
19859
|
function migrateYamlToSkillMd(yamlPath, outputDir) {
|
|
19079
|
-
const raw =
|
|
19860
|
+
const raw = fs6.readFileSync(yamlPath, "utf-8");
|
|
19080
19861
|
const parsed = YAML3.parse(raw);
|
|
19081
19862
|
if (!parsed?.id || !parsed?.name) {
|
|
19082
19863
|
throw new Error(`Invalid skill YAML: missing required fields (id, name)`);
|
|
19083
19864
|
}
|
|
19084
|
-
|
|
19865
|
+
fs6.mkdirSync(outputDir, { recursive: true });
|
|
19085
19866
|
const frontmatter = {
|
|
19086
19867
|
name: parsed.id,
|
|
19087
19868
|
description: parsed.description ?? ""
|
|
@@ -19095,15 +19876,15 @@ function migrateYamlToSkillMd(yamlPath, outputDir) {
|
|
|
19095
19876
|
const skillMd = matter2.stringify(wildcardPrompt ? `
|
|
19096
19877
|
${wildcardPrompt}
|
|
19097
19878
|
` : "\n", frontmatter);
|
|
19098
|
-
|
|
19879
|
+
fs6.writeFileSync(path6.join(outputDir, "SKILL.md"), skillMd, "utf-8");
|
|
19099
19880
|
if (promptFragments) {
|
|
19100
19881
|
const personaKeys = Object.keys(promptFragments).filter((k) => k !== "*");
|
|
19101
19882
|
if (personaKeys.length > 0) {
|
|
19102
|
-
const personasDir =
|
|
19103
|
-
|
|
19883
|
+
const personasDir = path6.join(outputDir, "personas");
|
|
19884
|
+
fs6.mkdirSync(personasDir, { recursive: true });
|
|
19104
19885
|
for (const personaId of personaKeys) {
|
|
19105
|
-
|
|
19106
|
-
|
|
19886
|
+
fs6.writeFileSync(
|
|
19887
|
+
path6.join(personasDir, `${personaId}.md`),
|
|
19107
19888
|
`${promptFragments[personaId]}
|
|
19108
19889
|
`,
|
|
19109
19890
|
"utf-8"
|
|
@@ -19113,8 +19894,8 @@ ${wildcardPrompt}
|
|
|
19113
19894
|
}
|
|
19114
19895
|
const actions = parsed.actions;
|
|
19115
19896
|
if (actions && actions.length > 0) {
|
|
19116
|
-
|
|
19117
|
-
|
|
19897
|
+
fs6.writeFileSync(
|
|
19898
|
+
path6.join(outputDir, "actions.yaml"),
|
|
19118
19899
|
YAML3.stringify(actions),
|
|
19119
19900
|
"utf-8"
|
|
19120
19901
|
);
|
|
@@ -19269,7 +20050,7 @@ function createWebTools(store, projectName, navGroups) {
|
|
|
19269
20050
|
content: [{ type: "text", text: JSON.stringify(urls, null, 2) }]
|
|
19270
20051
|
};
|
|
19271
20052
|
},
|
|
19272
|
-
{ annotations: {
|
|
20053
|
+
{ annotations: { readOnlyHint: true } }
|
|
19273
20054
|
),
|
|
19274
20055
|
tool20(
|
|
19275
20056
|
"get_dashboard_overview",
|
|
@@ -19291,7 +20072,7 @@ function createWebTools(store, projectName, navGroups) {
|
|
|
19291
20072
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
19292
20073
|
};
|
|
19293
20074
|
},
|
|
19294
|
-
{ annotations: {
|
|
20075
|
+
{ annotations: { readOnlyHint: true } }
|
|
19295
20076
|
),
|
|
19296
20077
|
tool20(
|
|
19297
20078
|
"get_dashboard_gar",
|
|
@@ -19303,7 +20084,7 @@ function createWebTools(store, projectName, navGroups) {
|
|
|
19303
20084
|
content: [{ type: "text", text: JSON.stringify(report, null, 2) }]
|
|
19304
20085
|
};
|
|
19305
20086
|
},
|
|
19306
|
-
{ annotations: {
|
|
20087
|
+
{ annotations: { readOnlyHint: true } }
|
|
19307
20088
|
),
|
|
19308
20089
|
tool20(
|
|
19309
20090
|
"get_dashboard_board",
|
|
@@ -19331,7 +20112,7 @@ function createWebTools(store, projectName, navGroups) {
|
|
|
19331
20112
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
19332
20113
|
};
|
|
19333
20114
|
},
|
|
19334
|
-
{ annotations: {
|
|
20115
|
+
{ annotations: { readOnlyHint: true } }
|
|
19335
20116
|
)
|
|
19336
20117
|
];
|
|
19337
20118
|
}
|
|
@@ -19357,8 +20138,8 @@ function createMarvinMcpServer(store, options) {
|
|
|
19357
20138
|
}
|
|
19358
20139
|
|
|
19359
20140
|
// src/agent/session.ts
|
|
19360
|
-
import * as
|
|
19361
|
-
import * as
|
|
20141
|
+
import * as fs9 from "fs";
|
|
20142
|
+
import * as path9 from "path";
|
|
19362
20143
|
import * as readline from "readline";
|
|
19363
20144
|
import chalk from "chalk";
|
|
19364
20145
|
import ora from "ora";
|
|
@@ -19367,13 +20148,13 @@ import {
|
|
|
19367
20148
|
} from "@anthropic-ai/claude-agent-sdk";
|
|
19368
20149
|
|
|
19369
20150
|
// src/storage/session-store.ts
|
|
19370
|
-
import * as
|
|
19371
|
-
import * as
|
|
20151
|
+
import * as fs7 from "fs";
|
|
20152
|
+
import * as path7 from "path";
|
|
19372
20153
|
import * as YAML4 from "yaml";
|
|
19373
20154
|
var SessionStore = class {
|
|
19374
20155
|
filePath;
|
|
19375
20156
|
constructor(marvinDir) {
|
|
19376
|
-
this.filePath =
|
|
20157
|
+
this.filePath = path7.join(marvinDir, "sessions.yaml");
|
|
19377
20158
|
}
|
|
19378
20159
|
list() {
|
|
19379
20160
|
const entries = this.load();
|
|
@@ -19414,9 +20195,9 @@ var SessionStore = class {
|
|
|
19414
20195
|
this.write(entries);
|
|
19415
20196
|
}
|
|
19416
20197
|
load() {
|
|
19417
|
-
if (!
|
|
20198
|
+
if (!fs7.existsSync(this.filePath)) return [];
|
|
19418
20199
|
try {
|
|
19419
|
-
const raw =
|
|
20200
|
+
const raw = fs7.readFileSync(this.filePath, "utf-8");
|
|
19420
20201
|
const parsed = YAML4.parse(raw);
|
|
19421
20202
|
if (!Array.isArray(parsed)) return [];
|
|
19422
20203
|
return parsed;
|
|
@@ -19425,11 +20206,11 @@ var SessionStore = class {
|
|
|
19425
20206
|
}
|
|
19426
20207
|
}
|
|
19427
20208
|
write(entries) {
|
|
19428
|
-
const dir =
|
|
19429
|
-
if (!
|
|
19430
|
-
|
|
20209
|
+
const dir = path7.dirname(this.filePath);
|
|
20210
|
+
if (!fs7.existsSync(dir)) {
|
|
20211
|
+
fs7.mkdirSync(dir, { recursive: true });
|
|
19431
20212
|
}
|
|
19432
|
-
|
|
20213
|
+
fs7.writeFileSync(this.filePath, YAML4.stringify(entries), "utf-8");
|
|
19433
20214
|
}
|
|
19434
20215
|
};
|
|
19435
20216
|
|
|
@@ -19465,8 +20246,8 @@ function slugify3(text) {
|
|
|
19465
20246
|
}
|
|
19466
20247
|
|
|
19467
20248
|
// src/sources/manifest.ts
|
|
19468
|
-
import * as
|
|
19469
|
-
import * as
|
|
20249
|
+
import * as fs8 from "fs";
|
|
20250
|
+
import * as path8 from "path";
|
|
19470
20251
|
import * as crypto from "crypto";
|
|
19471
20252
|
import * as YAML5 from "yaml";
|
|
19472
20253
|
var MANIFEST_FILE = ".manifest.yaml";
|
|
@@ -19479,37 +20260,37 @@ var SourceManifestManager = class {
|
|
|
19479
20260
|
manifestPath;
|
|
19480
20261
|
sourcesDir;
|
|
19481
20262
|
constructor(marvinDir) {
|
|
19482
|
-
this.sourcesDir =
|
|
19483
|
-
this.manifestPath =
|
|
20263
|
+
this.sourcesDir = path8.join(marvinDir, "sources");
|
|
20264
|
+
this.manifestPath = path8.join(this.sourcesDir, MANIFEST_FILE);
|
|
19484
20265
|
this.manifest = this.load();
|
|
19485
20266
|
}
|
|
19486
20267
|
load() {
|
|
19487
|
-
if (!
|
|
20268
|
+
if (!fs8.existsSync(this.manifestPath)) {
|
|
19488
20269
|
return emptyManifest();
|
|
19489
20270
|
}
|
|
19490
|
-
const raw =
|
|
20271
|
+
const raw = fs8.readFileSync(this.manifestPath, "utf-8");
|
|
19491
20272
|
const parsed = YAML5.parse(raw);
|
|
19492
20273
|
return parsed ?? emptyManifest();
|
|
19493
20274
|
}
|
|
19494
20275
|
save() {
|
|
19495
|
-
|
|
19496
|
-
|
|
20276
|
+
fs8.mkdirSync(this.sourcesDir, { recursive: true });
|
|
20277
|
+
fs8.writeFileSync(this.manifestPath, YAML5.stringify(this.manifest), "utf-8");
|
|
19497
20278
|
}
|
|
19498
20279
|
scan() {
|
|
19499
20280
|
const added = [];
|
|
19500
20281
|
const changed = [];
|
|
19501
20282
|
const removed = [];
|
|
19502
|
-
if (!
|
|
20283
|
+
if (!fs8.existsSync(this.sourcesDir)) {
|
|
19503
20284
|
return { added, changed, removed };
|
|
19504
20285
|
}
|
|
19505
20286
|
const onDisk = new Set(
|
|
19506
|
-
|
|
19507
|
-
const ext =
|
|
20287
|
+
fs8.readdirSync(this.sourcesDir).filter((f) => {
|
|
20288
|
+
const ext = path8.extname(f).toLowerCase();
|
|
19508
20289
|
return SOURCE_EXTENSIONS.includes(ext);
|
|
19509
20290
|
})
|
|
19510
20291
|
);
|
|
19511
20292
|
for (const fileName of onDisk) {
|
|
19512
|
-
const filePath =
|
|
20293
|
+
const filePath = path8.join(this.sourcesDir, fileName);
|
|
19513
20294
|
const hash2 = this.hashFile(filePath);
|
|
19514
20295
|
const existing = this.manifest.files[fileName];
|
|
19515
20296
|
if (!existing) {
|
|
@@ -19572,7 +20353,7 @@ var SourceManifestManager = class {
|
|
|
19572
20353
|
this.save();
|
|
19573
20354
|
}
|
|
19574
20355
|
hashFile(filePath) {
|
|
19575
|
-
const content =
|
|
20356
|
+
const content = fs8.readFileSync(filePath);
|
|
19576
20357
|
return crypto.createHash("sha256").update(content).digest("hex");
|
|
19577
20358
|
}
|
|
19578
20359
|
};
|
|
@@ -19587,8 +20368,8 @@ async function startSession(options) {
|
|
|
19587
20368
|
const skillRegistrations = collectSkillRegistrations(skillIds, allSkills);
|
|
19588
20369
|
const store = new DocumentStore(marvinDir, [...pluginRegistrations, ...skillRegistrations]);
|
|
19589
20370
|
const sessionStore = new SessionStore(marvinDir);
|
|
19590
|
-
const sourcesDir =
|
|
19591
|
-
const hasSourcesDir =
|
|
20371
|
+
const sourcesDir = path9.join(marvinDir, "sources");
|
|
20372
|
+
const hasSourcesDir = fs9.existsSync(sourcesDir);
|
|
19592
20373
|
const manifest = hasSourcesDir ? new SourceManifestManager(marvinDir) : void 0;
|
|
19593
20374
|
const pluginTools = plugin ? getPluginTools(plugin, store, marvinDir) : [];
|
|
19594
20375
|
const pluginPromptFragment = plugin ? getPluginPromptFragment(plugin, persona.id) : void 0;
|
|
@@ -19611,7 +20392,7 @@ async function startSession(options) {
|
|
|
19611
20392
|
projectName: config2.project.name,
|
|
19612
20393
|
navGroups
|
|
19613
20394
|
});
|
|
19614
|
-
const systemPrompt = buildSystemPrompt(persona, config2.project, pluginPromptFragment, skillPromptFragment);
|
|
20395
|
+
const systemPrompt = buildSystemPrompt(persona, config2.project, pluginPromptFragment, skillPromptFragment, marvinDir);
|
|
19615
20396
|
let existingSession;
|
|
19616
20397
|
if (options.sessionName) {
|
|
19617
20398
|
existingSession = sessionStore.get(options.sessionName);
|
|
@@ -19797,8 +20578,8 @@ Session ended with error: ${message.subtype}`));
|
|
|
19797
20578
|
}
|
|
19798
20579
|
|
|
19799
20580
|
// src/mcp/stdio-server.ts
|
|
19800
|
-
import * as
|
|
19801
|
-
import * as
|
|
20581
|
+
import * as fs10 from "fs";
|
|
20582
|
+
import * as path10 from "path";
|
|
19802
20583
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
19803
20584
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
19804
20585
|
|
|
@@ -20029,7 +20810,7 @@ ${summaries}`
|
|
|
20029
20810
|
content: [{ type: "text", text: guidance }]
|
|
20030
20811
|
};
|
|
20031
20812
|
},
|
|
20032
|
-
{ annotations: {
|
|
20813
|
+
{ annotations: { readOnlyHint: true } }
|
|
20033
20814
|
)
|
|
20034
20815
|
];
|
|
20035
20816
|
}
|
|
@@ -20096,8 +20877,8 @@ function collectTools(marvinDir) {
|
|
|
20096
20877
|
const plugin = resolvePlugin(config2.methodology);
|
|
20097
20878
|
const registrations = plugin?.documentTypeRegistrations ?? [];
|
|
20098
20879
|
const store = new DocumentStore(marvinDir, registrations);
|
|
20099
|
-
const sourcesDir =
|
|
20100
|
-
const hasSourcesDir =
|
|
20880
|
+
const sourcesDir = path10.join(marvinDir, "sources");
|
|
20881
|
+
const hasSourcesDir = fs10.existsSync(sourcesDir);
|
|
20101
20882
|
const manifest = hasSourcesDir ? new SourceManifestManager(marvinDir) : void 0;
|
|
20102
20883
|
const pluginTools = plugin ? getPluginTools(plugin, store, marvinDir) : [];
|
|
20103
20884
|
const sessionStore = new SessionStore(marvinDir);
|
|
@@ -20105,7 +20886,7 @@ function collectTools(marvinDir) {
|
|
|
20105
20886
|
const allSkillIds = [...allSkills.keys()];
|
|
20106
20887
|
const codeSkillTools = getSkillTools(allSkillIds, allSkills, store);
|
|
20107
20888
|
const skillsWithActions = allSkillIds.map((id) => allSkills.get(id)).filter((s) => s.actions && s.actions.length > 0);
|
|
20108
|
-
const projectRoot =
|
|
20889
|
+
const projectRoot = path10.dirname(marvinDir);
|
|
20109
20890
|
const actionTools = createSkillActionTools(skillsWithActions, { store, marvinDir, projectRoot });
|
|
20110
20891
|
return [
|
|
20111
20892
|
...createDecisionTools(store),
|
|
@@ -20165,11 +20946,63 @@ async function startStdioServer(options) {
|
|
|
20165
20946
|
import { Command } from "commander";
|
|
20166
20947
|
|
|
20167
20948
|
// src/cli/commands/init.ts
|
|
20168
|
-
import * as
|
|
20169
|
-
import * as
|
|
20949
|
+
import * as fs11 from "fs";
|
|
20950
|
+
import * as path11 from "path";
|
|
20170
20951
|
import * as YAML6 from "yaml";
|
|
20171
20952
|
import chalk2 from "chalk";
|
|
20172
20953
|
import { input, confirm, select } from "@inquirer/prompts";
|
|
20954
|
+
|
|
20955
|
+
// src/templates/claude-md.ts
|
|
20956
|
+
function getDefaultClaudeMdContent(projectName) {
|
|
20957
|
+
return `# Marvin \u2014 Project Instructions for "${projectName}"
|
|
20958
|
+
|
|
20959
|
+
You are **Marvin**, an AI-powered product development assistant.
|
|
20960
|
+
You operate as one of three personas \u2014 stay in role and suggest switching when a question falls outside your scope.
|
|
20961
|
+
|
|
20962
|
+
## Personas
|
|
20963
|
+
|
|
20964
|
+
| Persona | Short | Focus |
|
|
20965
|
+
|---------|-------|-------|
|
|
20966
|
+
| Product Owner | po | Vision, backlog, requirements, features, acceptance criteria |
|
|
20967
|
+
| Delivery Manager | dm | Planning, risks, actions, timelines, sprints, status |
|
|
20968
|
+
| Tech Lead | tl | Architecture, trade-offs, technical decisions, code quality |
|
|
20969
|
+
|
|
20970
|
+
## Proactive Governance
|
|
20971
|
+
|
|
20972
|
+
When conversation implies a commitment, risk, or open question, **suggest creating the matching artifact**:
|
|
20973
|
+
- A decision was made \u2192 offer to create a **Decision (D-xxx)**
|
|
20974
|
+
- Someone committed to a task \u2192 offer an **Action (A-xxx)** with owner and due date
|
|
20975
|
+
- An unanswered question surfaced \u2192 offer a **Question (Q-xxx)**
|
|
20976
|
+
- A new capability is discussed \u2192 offer a **Feature (F-xxx)**
|
|
20977
|
+
- Implementation scope is agreed \u2192 offer an **Epic (E-xxx)** linked to a feature
|
|
20978
|
+
- Work is being time-boxed \u2192 offer a **Sprint (SP-xxx)**
|
|
20979
|
+
|
|
20980
|
+
## Insights
|
|
20981
|
+
|
|
20982
|
+
Proactively flag:
|
|
20983
|
+
- Overdue actions or unresolved questions
|
|
20984
|
+
- Decisions without rationale or linked features
|
|
20985
|
+
- Features without linked epics
|
|
20986
|
+
- Risks mentioned but not tracked
|
|
20987
|
+
- When a risk is resolved \u2192 remove the "risk" tag and add "risk-mitigated"
|
|
20988
|
+
|
|
20989
|
+
## Tool Usage
|
|
20990
|
+
|
|
20991
|
+
- **Search before creating** \u2014 avoid duplicate artifacts
|
|
20992
|
+
- **Reference IDs** (e.g. D-001, A-003) when discussing existing items
|
|
20993
|
+
- **Link artifacts** \u2014 epics to features, actions to decisions, etc.
|
|
20994
|
+
- Use \`search_documents\` to find related context before answering
|
|
20995
|
+
|
|
20996
|
+
## Communication Style
|
|
20997
|
+
|
|
20998
|
+
- Be concise and structured
|
|
20999
|
+
- State assumptions explicitly
|
|
21000
|
+
- Use bullet points and tables where they aid clarity
|
|
21001
|
+
- When uncertain, ask a clarifying question rather than guessing
|
|
21002
|
+
`;
|
|
21003
|
+
}
|
|
21004
|
+
|
|
21005
|
+
// src/cli/commands/init.ts
|
|
20173
21006
|
async function initCommand() {
|
|
20174
21007
|
const cwd = process.cwd();
|
|
20175
21008
|
if (isMarvinProject(cwd)) {
|
|
@@ -20180,7 +21013,7 @@ async function initCommand() {
|
|
|
20180
21013
|
}
|
|
20181
21014
|
const projectName = await input({
|
|
20182
21015
|
message: "Project name:",
|
|
20183
|
-
default:
|
|
21016
|
+
default: path11.basename(cwd)
|
|
20184
21017
|
});
|
|
20185
21018
|
const methodology = await select({
|
|
20186
21019
|
message: "Methodology:",
|
|
@@ -20192,21 +21025,21 @@ async function initCommand() {
|
|
|
20192
21025
|
});
|
|
20193
21026
|
const plugin = resolvePlugin(methodology);
|
|
20194
21027
|
const registrations = plugin?.documentTypeRegistrations ?? [];
|
|
20195
|
-
const marvinDir =
|
|
21028
|
+
const marvinDir = path11.join(cwd, ".marvin");
|
|
20196
21029
|
const dirs = [
|
|
20197
21030
|
marvinDir,
|
|
20198
|
-
|
|
20199
|
-
|
|
20200
|
-
|
|
20201
|
-
|
|
20202
|
-
|
|
20203
|
-
|
|
21031
|
+
path11.join(marvinDir, "templates"),
|
|
21032
|
+
path11.join(marvinDir, "docs", "decisions"),
|
|
21033
|
+
path11.join(marvinDir, "docs", "actions"),
|
|
21034
|
+
path11.join(marvinDir, "docs", "questions"),
|
|
21035
|
+
path11.join(marvinDir, "sources"),
|
|
21036
|
+
path11.join(marvinDir, "skills")
|
|
20204
21037
|
];
|
|
20205
21038
|
for (const reg of registrations) {
|
|
20206
|
-
dirs.push(
|
|
21039
|
+
dirs.push(path11.join(marvinDir, "docs", reg.dirName));
|
|
20207
21040
|
}
|
|
20208
21041
|
for (const dir of dirs) {
|
|
20209
|
-
|
|
21042
|
+
fs11.mkdirSync(dir, { recursive: true });
|
|
20210
21043
|
}
|
|
20211
21044
|
const config2 = {
|
|
20212
21045
|
name: projectName,
|
|
@@ -20220,16 +21053,22 @@ async function initCommand() {
|
|
|
20220
21053
|
if (methodology === "sap-aem") {
|
|
20221
21054
|
config2.aem = { currentPhase: "assess-use-case" };
|
|
20222
21055
|
}
|
|
20223
|
-
|
|
20224
|
-
|
|
21056
|
+
fs11.writeFileSync(
|
|
21057
|
+
path11.join(marvinDir, "config.yaml"),
|
|
20225
21058
|
YAML6.stringify(config2),
|
|
20226
21059
|
"utf-8"
|
|
20227
21060
|
);
|
|
21061
|
+
fs11.writeFileSync(
|
|
21062
|
+
path11.join(marvinDir, "CLAUDE.md"),
|
|
21063
|
+
getDefaultClaudeMdContent(projectName),
|
|
21064
|
+
"utf-8"
|
|
21065
|
+
);
|
|
20228
21066
|
console.log(chalk2.green(`
|
|
20229
21067
|
Initialized Marvin project "${projectName}" in ${cwd}`));
|
|
20230
21068
|
console.log(chalk2.dim(`Methodology: ${plugin?.name ?? methodology}`));
|
|
20231
21069
|
console.log(chalk2.dim("\nCreated:"));
|
|
20232
21070
|
console.log(chalk2.dim(" .marvin/config.yaml"));
|
|
21071
|
+
console.log(chalk2.dim(" .marvin/CLAUDE.md"));
|
|
20233
21072
|
console.log(chalk2.dim(" .marvin/docs/decisions/"));
|
|
20234
21073
|
console.log(chalk2.dim(" .marvin/docs/actions/"));
|
|
20235
21074
|
console.log(chalk2.dim(" .marvin/docs/questions/"));
|
|
@@ -20247,18 +21086,18 @@ Initialized Marvin project "${projectName}" in ${cwd}`));
|
|
|
20247
21086
|
const sourceDir = await input({
|
|
20248
21087
|
message: "Path to directory containing source documents:"
|
|
20249
21088
|
});
|
|
20250
|
-
const resolvedDir =
|
|
20251
|
-
if (
|
|
21089
|
+
const resolvedDir = path11.resolve(sourceDir);
|
|
21090
|
+
if (fs11.existsSync(resolvedDir) && fs11.statSync(resolvedDir).isDirectory()) {
|
|
20252
21091
|
const sourceExts = [".pdf", ".md", ".txt"];
|
|
20253
|
-
const files =
|
|
20254
|
-
const ext =
|
|
21092
|
+
const files = fs11.readdirSync(resolvedDir).filter((f) => {
|
|
21093
|
+
const ext = path11.extname(f).toLowerCase();
|
|
20255
21094
|
return sourceExts.includes(ext);
|
|
20256
21095
|
});
|
|
20257
21096
|
let copied = 0;
|
|
20258
21097
|
for (const file2 of files) {
|
|
20259
|
-
const src =
|
|
20260
|
-
const dest =
|
|
20261
|
-
|
|
21098
|
+
const src = path11.join(resolvedDir, file2);
|
|
21099
|
+
const dest = path11.join(marvinDir, "sources", file2);
|
|
21100
|
+
fs11.copyFileSync(src, dest);
|
|
20262
21101
|
copied++;
|
|
20263
21102
|
}
|
|
20264
21103
|
if (copied > 0) {
|
|
@@ -20548,13 +21387,13 @@ async function setApiKey() {
|
|
|
20548
21387
|
}
|
|
20549
21388
|
|
|
20550
21389
|
// src/cli/commands/ingest.ts
|
|
20551
|
-
import * as
|
|
20552
|
-
import * as
|
|
21390
|
+
import * as fs13 from "fs";
|
|
21391
|
+
import * as path13 from "path";
|
|
20553
21392
|
import chalk8 from "chalk";
|
|
20554
21393
|
|
|
20555
21394
|
// src/sources/ingest.ts
|
|
20556
|
-
import * as
|
|
20557
|
-
import * as
|
|
21395
|
+
import * as fs12 from "fs";
|
|
21396
|
+
import * as path12 from "path";
|
|
20558
21397
|
import chalk7 from "chalk";
|
|
20559
21398
|
import ora2 from "ora";
|
|
20560
21399
|
import { query as query4 } from "@anthropic-ai/claude-agent-sdk";
|
|
@@ -20657,15 +21496,15 @@ async function ingestFile(options) {
|
|
|
20657
21496
|
const persona = getPersona(personaId);
|
|
20658
21497
|
const manifest = new SourceManifestManager(marvinDir);
|
|
20659
21498
|
const sourcesDir = manifest.sourcesDir;
|
|
20660
|
-
const filePath =
|
|
20661
|
-
if (!
|
|
21499
|
+
const filePath = path12.join(sourcesDir, fileName);
|
|
21500
|
+
if (!fs12.existsSync(filePath)) {
|
|
20662
21501
|
throw new Error(`Source file not found: ${filePath}`);
|
|
20663
21502
|
}
|
|
20664
|
-
const ext =
|
|
21503
|
+
const ext = path12.extname(fileName).toLowerCase();
|
|
20665
21504
|
const isPdf = ext === ".pdf";
|
|
20666
21505
|
let fileContent = null;
|
|
20667
21506
|
if (!isPdf) {
|
|
20668
|
-
fileContent =
|
|
21507
|
+
fileContent = fs12.readFileSync(filePath, "utf-8");
|
|
20669
21508
|
}
|
|
20670
21509
|
const store = new DocumentStore(marvinDir);
|
|
20671
21510
|
const createdArtifacts = [];
|
|
@@ -20768,9 +21607,9 @@ Ingest ended with error: ${message.subtype}`)
|
|
|
20768
21607
|
async function ingestCommand(file2, options) {
|
|
20769
21608
|
const project = loadProject();
|
|
20770
21609
|
const marvinDir = project.marvinDir;
|
|
20771
|
-
const sourcesDir =
|
|
20772
|
-
if (!
|
|
20773
|
-
|
|
21610
|
+
const sourcesDir = path13.join(marvinDir, "sources");
|
|
21611
|
+
if (!fs13.existsSync(sourcesDir)) {
|
|
21612
|
+
fs13.mkdirSync(sourcesDir, { recursive: true });
|
|
20774
21613
|
}
|
|
20775
21614
|
const manifest = new SourceManifestManager(marvinDir);
|
|
20776
21615
|
manifest.scan();
|
|
@@ -20781,8 +21620,8 @@ async function ingestCommand(file2, options) {
|
|
|
20781
21620
|
return;
|
|
20782
21621
|
}
|
|
20783
21622
|
if (file2) {
|
|
20784
|
-
const filePath =
|
|
20785
|
-
if (!
|
|
21623
|
+
const filePath = path13.join(sourcesDir, file2);
|
|
21624
|
+
if (!fs13.existsSync(filePath)) {
|
|
20786
21625
|
console.log(chalk8.red(`Source file not found: ${file2}`));
|
|
20787
21626
|
console.log(chalk8.dim(`Expected at: ${filePath}`));
|
|
20788
21627
|
console.log(chalk8.dim(`Drop files into .marvin/sources/ and try again.`));
|
|
@@ -20849,7 +21688,7 @@ import ora3 from "ora";
|
|
|
20849
21688
|
import { input as input3 } from "@inquirer/prompts";
|
|
20850
21689
|
|
|
20851
21690
|
// src/git/repository.ts
|
|
20852
|
-
import * as
|
|
21691
|
+
import * as path14 from "path";
|
|
20853
21692
|
import simpleGit from "simple-git";
|
|
20854
21693
|
var MARVIN_GITIGNORE = `node_modules/
|
|
20855
21694
|
.DS_Store
|
|
@@ -20869,7 +21708,7 @@ var DIR_TYPE_LABELS = {
|
|
|
20869
21708
|
function buildCommitMessage(files) {
|
|
20870
21709
|
const counts = /* @__PURE__ */ new Map();
|
|
20871
21710
|
for (const f of files) {
|
|
20872
|
-
const parts2 = f.split(
|
|
21711
|
+
const parts2 = f.split(path14.sep).join("/").split("/");
|
|
20873
21712
|
const docsIdx = parts2.indexOf("docs");
|
|
20874
21713
|
if (docsIdx !== -1 && docsIdx + 1 < parts2.length) {
|
|
20875
21714
|
const dirName = parts2[docsIdx + 1];
|
|
@@ -20909,9 +21748,9 @@ var MarvinGit = class {
|
|
|
20909
21748
|
);
|
|
20910
21749
|
}
|
|
20911
21750
|
await this.git.init();
|
|
20912
|
-
const { writeFileSync:
|
|
20913
|
-
|
|
20914
|
-
|
|
21751
|
+
const { writeFileSync: writeFileSync10 } = await import("fs");
|
|
21752
|
+
writeFileSync10(
|
|
21753
|
+
path14.join(this.marvinDir, ".gitignore"),
|
|
20915
21754
|
MARVIN_GITIGNORE,
|
|
20916
21755
|
"utf-8"
|
|
20917
21756
|
);
|
|
@@ -21031,9 +21870,9 @@ var MarvinGit = class {
|
|
|
21031
21870
|
}
|
|
21032
21871
|
}
|
|
21033
21872
|
static async clone(url2, targetDir) {
|
|
21034
|
-
const marvinDir =
|
|
21035
|
-
const { existsSync:
|
|
21036
|
-
if (
|
|
21873
|
+
const marvinDir = path14.join(targetDir, ".marvin");
|
|
21874
|
+
const { existsSync: existsSync17 } = await import("fs");
|
|
21875
|
+
if (existsSync17(marvinDir)) {
|
|
21037
21876
|
throw new GitSyncError(
|
|
21038
21877
|
`.marvin/ already exists at ${targetDir}. Remove it first or choose a different directory.`
|
|
21039
21878
|
);
|
|
@@ -21217,8 +22056,8 @@ async function serveCommand() {
|
|
|
21217
22056
|
}
|
|
21218
22057
|
|
|
21219
22058
|
// src/cli/commands/skills.ts
|
|
21220
|
-
import * as
|
|
21221
|
-
import * as
|
|
22059
|
+
import * as fs14 from "fs";
|
|
22060
|
+
import * as path15 from "path";
|
|
21222
22061
|
import * as YAML7 from "yaml";
|
|
21223
22062
|
import matter3 from "gray-matter";
|
|
21224
22063
|
import chalk10 from "chalk";
|
|
@@ -21324,14 +22163,14 @@ async function skillsRemoveCommand(skillId, options) {
|
|
|
21324
22163
|
}
|
|
21325
22164
|
async function skillsCreateCommand(name) {
|
|
21326
22165
|
const project = loadProject();
|
|
21327
|
-
const skillsDir =
|
|
21328
|
-
|
|
21329
|
-
const skillDir =
|
|
21330
|
-
if (
|
|
22166
|
+
const skillsDir = path15.join(project.marvinDir, "skills");
|
|
22167
|
+
fs14.mkdirSync(skillsDir, { recursive: true });
|
|
22168
|
+
const skillDir = path15.join(skillsDir, name);
|
|
22169
|
+
if (fs14.existsSync(skillDir)) {
|
|
21331
22170
|
console.log(chalk10.yellow(`Skill directory already exists: ${skillDir}`));
|
|
21332
22171
|
return;
|
|
21333
22172
|
}
|
|
21334
|
-
|
|
22173
|
+
fs14.mkdirSync(skillDir, { recursive: true });
|
|
21335
22174
|
const displayName = name.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
21336
22175
|
const frontmatter = {
|
|
21337
22176
|
name,
|
|
@@ -21345,7 +22184,7 @@ async function skillsCreateCommand(name) {
|
|
|
21345
22184
|
You have the **${displayName}** skill.
|
|
21346
22185
|
`;
|
|
21347
22186
|
const skillMd = matter3.stringify(body, frontmatter);
|
|
21348
|
-
|
|
22187
|
+
fs14.writeFileSync(path15.join(skillDir, "SKILL.md"), skillMd, "utf-8");
|
|
21349
22188
|
const actions = [
|
|
21350
22189
|
{
|
|
21351
22190
|
id: "run",
|
|
@@ -21355,7 +22194,7 @@ You have the **${displayName}** skill.
|
|
|
21355
22194
|
maxTurns: 5
|
|
21356
22195
|
}
|
|
21357
22196
|
];
|
|
21358
|
-
|
|
22197
|
+
fs14.writeFileSync(path15.join(skillDir, "actions.yaml"), YAML7.stringify(actions), "utf-8");
|
|
21359
22198
|
console.log(chalk10.green(`Created skill: ${skillDir}/`));
|
|
21360
22199
|
console.log(chalk10.dim(" SKILL.md \u2014 skill definition and prompt"));
|
|
21361
22200
|
console.log(chalk10.dim(" actions.yaml \u2014 action definitions"));
|
|
@@ -21363,14 +22202,14 @@ You have the **${displayName}** skill.
|
|
|
21363
22202
|
}
|
|
21364
22203
|
async function skillsMigrateCommand() {
|
|
21365
22204
|
const project = loadProject();
|
|
21366
|
-
const skillsDir =
|
|
21367
|
-
if (!
|
|
22205
|
+
const skillsDir = path15.join(project.marvinDir, "skills");
|
|
22206
|
+
if (!fs14.existsSync(skillsDir)) {
|
|
21368
22207
|
console.log(chalk10.dim("No skills directory found."));
|
|
21369
22208
|
return;
|
|
21370
22209
|
}
|
|
21371
22210
|
let entries;
|
|
21372
22211
|
try {
|
|
21373
|
-
entries =
|
|
22212
|
+
entries = fs14.readdirSync(skillsDir);
|
|
21374
22213
|
} catch {
|
|
21375
22214
|
console.log(chalk10.red("Could not read skills directory."));
|
|
21376
22215
|
return;
|
|
@@ -21382,16 +22221,16 @@ async function skillsMigrateCommand() {
|
|
|
21382
22221
|
}
|
|
21383
22222
|
let migrated = 0;
|
|
21384
22223
|
for (const file2 of yamlFiles) {
|
|
21385
|
-
const yamlPath =
|
|
22224
|
+
const yamlPath = path15.join(skillsDir, file2);
|
|
21386
22225
|
const baseName = file2.replace(/\.(yaml|yml)$/, "");
|
|
21387
|
-
const outputDir =
|
|
21388
|
-
if (
|
|
22226
|
+
const outputDir = path15.join(skillsDir, baseName);
|
|
22227
|
+
if (fs14.existsSync(outputDir)) {
|
|
21389
22228
|
console.log(chalk10.yellow(`Skipping "${file2}" \u2014 directory "${baseName}/" already exists.`));
|
|
21390
22229
|
continue;
|
|
21391
22230
|
}
|
|
21392
22231
|
try {
|
|
21393
22232
|
migrateYamlToSkillMd(yamlPath, outputDir);
|
|
21394
|
-
|
|
22233
|
+
fs14.renameSync(yamlPath, `${yamlPath}.bak`);
|
|
21395
22234
|
console.log(chalk10.green(`Migrated "${file2}" \u2192 "${baseName}/"`));
|
|
21396
22235
|
migrated++;
|
|
21397
22236
|
} catch (err) {
|
|
@@ -21405,35 +22244,35 @@ ${migrated} skill(s) migrated. Original files renamed to *.bak`));
|
|
|
21405
22244
|
}
|
|
21406
22245
|
|
|
21407
22246
|
// src/cli/commands/import.ts
|
|
21408
|
-
import * as
|
|
21409
|
-
import * as
|
|
22247
|
+
import * as fs17 from "fs";
|
|
22248
|
+
import * as path18 from "path";
|
|
21410
22249
|
import chalk11 from "chalk";
|
|
21411
22250
|
|
|
21412
22251
|
// src/import/engine.ts
|
|
21413
|
-
import * as
|
|
21414
|
-
import * as
|
|
22252
|
+
import * as fs16 from "fs";
|
|
22253
|
+
import * as path17 from "path";
|
|
21415
22254
|
import matter5 from "gray-matter";
|
|
21416
22255
|
|
|
21417
22256
|
// src/import/classifier.ts
|
|
21418
|
-
import * as
|
|
21419
|
-
import * as
|
|
22257
|
+
import * as fs15 from "fs";
|
|
22258
|
+
import * as path16 from "path";
|
|
21420
22259
|
import matter4 from "gray-matter";
|
|
21421
22260
|
var RAW_SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".pdf", ".txt"]);
|
|
21422
22261
|
var CORE_DIR_NAMES = /* @__PURE__ */ new Set(["decisions", "actions", "questions"]);
|
|
21423
22262
|
var ID_PATTERN = /^[A-Z]+-\d{3,}$/;
|
|
21424
22263
|
function classifyPath(inputPath, knownTypes, knownDirNames) {
|
|
21425
|
-
const resolved =
|
|
21426
|
-
const stat =
|
|
22264
|
+
const resolved = path16.resolve(inputPath);
|
|
22265
|
+
const stat = fs15.statSync(resolved);
|
|
21427
22266
|
if (!stat.isDirectory()) {
|
|
21428
22267
|
return classifyFile(resolved, knownTypes);
|
|
21429
22268
|
}
|
|
21430
|
-
if (
|
|
22269
|
+
if (path16.basename(resolved) === ".marvin" || fs15.existsSync(path16.join(resolved, "config.yaml"))) {
|
|
21431
22270
|
return { type: "marvin-project", inputPath: resolved };
|
|
21432
22271
|
}
|
|
21433
22272
|
const allDirNames = /* @__PURE__ */ new Set([...CORE_DIR_NAMES, ...knownDirNames]);
|
|
21434
|
-
const entries =
|
|
22273
|
+
const entries = fs15.readdirSync(resolved);
|
|
21435
22274
|
const hasDocSubdirs = entries.some(
|
|
21436
|
-
(e) => allDirNames.has(e) &&
|
|
22275
|
+
(e) => allDirNames.has(e) && fs15.statSync(path16.join(resolved, e)).isDirectory()
|
|
21437
22276
|
);
|
|
21438
22277
|
if (hasDocSubdirs) {
|
|
21439
22278
|
return { type: "docs-directory", inputPath: resolved };
|
|
@@ -21442,7 +22281,7 @@ function classifyPath(inputPath, knownTypes, knownDirNames) {
|
|
|
21442
22281
|
if (mdFiles.length > 0) {
|
|
21443
22282
|
const hasMarvinDocs = mdFiles.some((f) => {
|
|
21444
22283
|
try {
|
|
21445
|
-
const raw =
|
|
22284
|
+
const raw = fs15.readFileSync(path16.join(resolved, f), "utf-8");
|
|
21446
22285
|
const { data } = matter4(raw);
|
|
21447
22286
|
return isValidMarvinDocument(data, knownTypes);
|
|
21448
22287
|
} catch {
|
|
@@ -21456,14 +22295,14 @@ function classifyPath(inputPath, knownTypes, knownDirNames) {
|
|
|
21456
22295
|
return { type: "raw-source-dir", inputPath: resolved };
|
|
21457
22296
|
}
|
|
21458
22297
|
function classifyFile(filePath, knownTypes) {
|
|
21459
|
-
const resolved =
|
|
21460
|
-
const ext =
|
|
22298
|
+
const resolved = path16.resolve(filePath);
|
|
22299
|
+
const ext = path16.extname(resolved).toLowerCase();
|
|
21461
22300
|
if (RAW_SOURCE_EXTENSIONS.has(ext)) {
|
|
21462
22301
|
return { type: "raw-source-file", inputPath: resolved };
|
|
21463
22302
|
}
|
|
21464
22303
|
if (ext === ".md") {
|
|
21465
22304
|
try {
|
|
21466
|
-
const raw =
|
|
22305
|
+
const raw = fs15.readFileSync(resolved, "utf-8");
|
|
21467
22306
|
const { data } = matter4(raw);
|
|
21468
22307
|
if (isValidMarvinDocument(data, knownTypes)) {
|
|
21469
22308
|
return { type: "marvin-document", inputPath: resolved };
|
|
@@ -21588,9 +22427,9 @@ function executeImportPlan(plan, store, marvinDir, options) {
|
|
|
21588
22427
|
continue;
|
|
21589
22428
|
}
|
|
21590
22429
|
if (item.action === "copy") {
|
|
21591
|
-
const targetDir =
|
|
21592
|
-
|
|
21593
|
-
|
|
22430
|
+
const targetDir = path17.dirname(item.targetPath);
|
|
22431
|
+
fs16.mkdirSync(targetDir, { recursive: true });
|
|
22432
|
+
fs16.copyFileSync(item.sourcePath, item.targetPath);
|
|
21594
22433
|
copied++;
|
|
21595
22434
|
continue;
|
|
21596
22435
|
}
|
|
@@ -21626,19 +22465,19 @@ function formatPlanSummary(plan) {
|
|
|
21626
22465
|
lines.push(`Documents to import: ${imports.length}`);
|
|
21627
22466
|
for (const item of imports) {
|
|
21628
22467
|
const idInfo = item.originalId !== item.newId ? `${item.originalId} \u2192 ${item.newId}` : item.newId ?? item.originalId ?? "";
|
|
21629
|
-
lines.push(` ${idInfo} ${
|
|
22468
|
+
lines.push(` ${idInfo} ${path17.basename(item.sourcePath)}`);
|
|
21630
22469
|
}
|
|
21631
22470
|
}
|
|
21632
22471
|
if (copies.length > 0) {
|
|
21633
22472
|
lines.push(`Files to copy to sources/: ${copies.length}`);
|
|
21634
22473
|
for (const item of copies) {
|
|
21635
|
-
lines.push(` ${
|
|
22474
|
+
lines.push(` ${path17.basename(item.sourcePath)} \u2192 ${path17.basename(item.targetPath)}`);
|
|
21636
22475
|
}
|
|
21637
22476
|
}
|
|
21638
22477
|
if (skips.length > 0) {
|
|
21639
22478
|
lines.push(`Skipped (conflict): ${skips.length}`);
|
|
21640
22479
|
for (const item of skips) {
|
|
21641
|
-
lines.push(` ${item.originalId ??
|
|
22480
|
+
lines.push(` ${item.originalId ?? path17.basename(item.sourcePath)} ${item.reason ?? ""}`);
|
|
21642
22481
|
}
|
|
21643
22482
|
}
|
|
21644
22483
|
if (plan.items.length === 0) {
|
|
@@ -21671,11 +22510,11 @@ function getDirNameForType(store, type) {
|
|
|
21671
22510
|
}
|
|
21672
22511
|
function collectMarvinDocs(dir, knownTypes) {
|
|
21673
22512
|
const docs = [];
|
|
21674
|
-
const files =
|
|
22513
|
+
const files = fs16.readdirSync(dir).filter((f) => f.endsWith(".md"));
|
|
21675
22514
|
for (const file2 of files) {
|
|
21676
|
-
const filePath =
|
|
22515
|
+
const filePath = path17.join(dir, file2);
|
|
21677
22516
|
try {
|
|
21678
|
-
const raw =
|
|
22517
|
+
const raw = fs16.readFileSync(filePath, "utf-8");
|
|
21679
22518
|
const { data, content } = matter5(raw);
|
|
21680
22519
|
if (isValidMarvinDocument(data, knownTypes)) {
|
|
21681
22520
|
docs.push({
|
|
@@ -21731,23 +22570,23 @@ function planDocImports(docs, store, options) {
|
|
|
21731
22570
|
}
|
|
21732
22571
|
function planFromMarvinProject(classification, store, _marvinDir, options) {
|
|
21733
22572
|
let projectDir = classification.inputPath;
|
|
21734
|
-
if (
|
|
21735
|
-
const inner =
|
|
21736
|
-
if (
|
|
22573
|
+
if (path17.basename(projectDir) !== ".marvin") {
|
|
22574
|
+
const inner = path17.join(projectDir, ".marvin");
|
|
22575
|
+
if (fs16.existsSync(inner)) {
|
|
21737
22576
|
projectDir = inner;
|
|
21738
22577
|
}
|
|
21739
22578
|
}
|
|
21740
|
-
const docsDir =
|
|
21741
|
-
if (!
|
|
22579
|
+
const docsDir = path17.join(projectDir, "docs");
|
|
22580
|
+
if (!fs16.existsSync(docsDir)) {
|
|
21742
22581
|
return [];
|
|
21743
22582
|
}
|
|
21744
22583
|
const knownTypes = store.registeredTypes;
|
|
21745
22584
|
const allDocs = [];
|
|
21746
|
-
const subdirs =
|
|
21747
|
-
(d) =>
|
|
22585
|
+
const subdirs = fs16.readdirSync(docsDir).filter(
|
|
22586
|
+
(d) => fs16.statSync(path17.join(docsDir, d)).isDirectory()
|
|
21748
22587
|
);
|
|
21749
22588
|
for (const subdir of subdirs) {
|
|
21750
|
-
const docs = collectMarvinDocs(
|
|
22589
|
+
const docs = collectMarvinDocs(path17.join(docsDir, subdir), knownTypes);
|
|
21751
22590
|
allDocs.push(...docs);
|
|
21752
22591
|
}
|
|
21753
22592
|
return planDocImports(allDocs, store, options);
|
|
@@ -21757,10 +22596,10 @@ function planFromDocsDirectory(classification, store, _marvinDir, options) {
|
|
|
21757
22596
|
const knownTypes = store.registeredTypes;
|
|
21758
22597
|
const allDocs = [];
|
|
21759
22598
|
allDocs.push(...collectMarvinDocs(dir, knownTypes));
|
|
21760
|
-
const entries =
|
|
22599
|
+
const entries = fs16.readdirSync(dir);
|
|
21761
22600
|
for (const entry of entries) {
|
|
21762
|
-
const entryPath =
|
|
21763
|
-
if (
|
|
22601
|
+
const entryPath = path17.join(dir, entry);
|
|
22602
|
+
if (fs16.statSync(entryPath).isDirectory()) {
|
|
21764
22603
|
allDocs.push(...collectMarvinDocs(entryPath, knownTypes));
|
|
21765
22604
|
}
|
|
21766
22605
|
}
|
|
@@ -21769,7 +22608,7 @@ function planFromDocsDirectory(classification, store, _marvinDir, options) {
|
|
|
21769
22608
|
function planFromSingleDocument(classification, store, _marvinDir, options) {
|
|
21770
22609
|
const filePath = classification.inputPath;
|
|
21771
22610
|
const knownTypes = store.registeredTypes;
|
|
21772
|
-
const raw =
|
|
22611
|
+
const raw = fs16.readFileSync(filePath, "utf-8");
|
|
21773
22612
|
const { data, content } = matter5(raw);
|
|
21774
22613
|
if (!isValidMarvinDocument(data, knownTypes)) {
|
|
21775
22614
|
return [];
|
|
@@ -21785,14 +22624,14 @@ function planFromSingleDocument(classification, store, _marvinDir, options) {
|
|
|
21785
22624
|
}
|
|
21786
22625
|
function planFromRawSourceDir(classification, marvinDir) {
|
|
21787
22626
|
const dir = classification.inputPath;
|
|
21788
|
-
const sourcesDir =
|
|
22627
|
+
const sourcesDir = path17.join(marvinDir, "sources");
|
|
21789
22628
|
const items = [];
|
|
21790
|
-
const files =
|
|
21791
|
-
const stat =
|
|
22629
|
+
const files = fs16.readdirSync(dir).filter((f) => {
|
|
22630
|
+
const stat = fs16.statSync(path17.join(dir, f));
|
|
21792
22631
|
return stat.isFile();
|
|
21793
22632
|
});
|
|
21794
22633
|
for (const file2 of files) {
|
|
21795
|
-
const sourcePath =
|
|
22634
|
+
const sourcePath = path17.join(dir, file2);
|
|
21796
22635
|
const targetPath = resolveSourceFileName(sourcesDir, file2);
|
|
21797
22636
|
items.push({
|
|
21798
22637
|
action: "copy",
|
|
@@ -21803,8 +22642,8 @@ function planFromRawSourceDir(classification, marvinDir) {
|
|
|
21803
22642
|
return items;
|
|
21804
22643
|
}
|
|
21805
22644
|
function planFromRawSourceFile(classification, marvinDir) {
|
|
21806
|
-
const sourcesDir =
|
|
21807
|
-
const fileName =
|
|
22645
|
+
const sourcesDir = path17.join(marvinDir, "sources");
|
|
22646
|
+
const fileName = path17.basename(classification.inputPath);
|
|
21808
22647
|
const targetPath = resolveSourceFileName(sourcesDir, fileName);
|
|
21809
22648
|
return [
|
|
21810
22649
|
{
|
|
@@ -21815,25 +22654,25 @@ function planFromRawSourceFile(classification, marvinDir) {
|
|
|
21815
22654
|
];
|
|
21816
22655
|
}
|
|
21817
22656
|
function resolveSourceFileName(sourcesDir, fileName) {
|
|
21818
|
-
const targetPath =
|
|
21819
|
-
if (!
|
|
22657
|
+
const targetPath = path17.join(sourcesDir, fileName);
|
|
22658
|
+
if (!fs16.existsSync(targetPath)) {
|
|
21820
22659
|
return targetPath;
|
|
21821
22660
|
}
|
|
21822
|
-
const ext =
|
|
21823
|
-
const base =
|
|
22661
|
+
const ext = path17.extname(fileName);
|
|
22662
|
+
const base = path17.basename(fileName, ext);
|
|
21824
22663
|
let counter = 1;
|
|
21825
22664
|
let candidate;
|
|
21826
22665
|
do {
|
|
21827
|
-
candidate =
|
|
22666
|
+
candidate = path17.join(sourcesDir, `${base}-${counter}${ext}`);
|
|
21828
22667
|
counter++;
|
|
21829
|
-
} while (
|
|
22668
|
+
} while (fs16.existsSync(candidate));
|
|
21830
22669
|
return candidate;
|
|
21831
22670
|
}
|
|
21832
22671
|
|
|
21833
22672
|
// src/cli/commands/import.ts
|
|
21834
22673
|
async function importCommand(inputPath, options) {
|
|
21835
|
-
const resolved =
|
|
21836
|
-
if (!
|
|
22674
|
+
const resolved = path18.resolve(inputPath);
|
|
22675
|
+
if (!fs17.existsSync(resolved)) {
|
|
21837
22676
|
throw new ImportError(`Path not found: ${resolved}`);
|
|
21838
22677
|
}
|
|
21839
22678
|
const project = loadProject();
|
|
@@ -21885,7 +22724,7 @@ async function importCommand(inputPath, options) {
|
|
|
21885
22724
|
console.log(chalk11.bold("\nStarting ingest of copied sources...\n"));
|
|
21886
22725
|
const manifest = new SourceManifestManager(marvinDir);
|
|
21887
22726
|
manifest.scan();
|
|
21888
|
-
const copiedFileNames = result.items.filter((i) => i.action === "copy").map((i) =>
|
|
22727
|
+
const copiedFileNames = result.items.filter((i) => i.action === "copy").map((i) => path18.basename(i.targetPath));
|
|
21889
22728
|
for (const fileName of copiedFileNames) {
|
|
21890
22729
|
try {
|
|
21891
22730
|
await ingestFile({
|
|
@@ -22288,7 +23127,8 @@ The contributor is identifying a project risk.
|
|
|
22288
23127
|
- Create actions for risk mitigation tasks
|
|
22289
23128
|
- Create decisions for risk response strategies
|
|
22290
23129
|
- Create questions for risks needing further assessment
|
|
22291
|
-
- Tag all related artifacts with "risk" for tracking
|
|
23130
|
+
- Tag all related artifacts with "risk" for tracking
|
|
23131
|
+
- When a risk is resolved, use the update tool to remove the "risk" tag and add "risk-mitigated" so it no longer inflates the GAR quality metric`,
|
|
22292
23132
|
"blocker-report": `
|
|
22293
23133
|
### Type-Specific Guidance: Blocker Report
|
|
22294
23134
|
The contributor is reporting a blocker.
|
|
@@ -22633,6 +23473,105 @@ function renderConfluence(report) {
|
|
|
22633
23473
|
return lines.join("\n");
|
|
22634
23474
|
}
|
|
22635
23475
|
|
|
23476
|
+
// src/reports/health/render-ascii.ts
|
|
23477
|
+
import chalk17 from "chalk";
|
|
23478
|
+
var STATUS_DOT2 = {
|
|
23479
|
+
green: chalk17.green("\u25CF"),
|
|
23480
|
+
amber: chalk17.yellow("\u25CF"),
|
|
23481
|
+
red: chalk17.red("\u25CF")
|
|
23482
|
+
};
|
|
23483
|
+
var STATUS_LABEL2 = {
|
|
23484
|
+
green: chalk17.green.bold("GREEN"),
|
|
23485
|
+
amber: chalk17.yellow.bold("AMBER"),
|
|
23486
|
+
red: chalk17.red.bold("RED")
|
|
23487
|
+
};
|
|
23488
|
+
var SEPARATOR2 = chalk17.dim("\u2500".repeat(60));
|
|
23489
|
+
function renderAscii2(report) {
|
|
23490
|
+
const lines = [];
|
|
23491
|
+
lines.push("");
|
|
23492
|
+
lines.push(chalk17.bold(` Health Check \xB7 ${report.projectName}`));
|
|
23493
|
+
lines.push(chalk17.dim(` ${report.generatedAt}`));
|
|
23494
|
+
lines.push("");
|
|
23495
|
+
lines.push(` Overall: ${STATUS_LABEL2[report.overall]}`);
|
|
23496
|
+
lines.push("");
|
|
23497
|
+
lines.push(` ${SEPARATOR2}`);
|
|
23498
|
+
lines.push(chalk17.bold(" Completeness"));
|
|
23499
|
+
lines.push(` ${SEPARATOR2}`);
|
|
23500
|
+
for (const cat of report.completeness) {
|
|
23501
|
+
lines.push(` ${STATUS_DOT2[cat.status]} ${chalk17.bold(cat.name.padEnd(16))} ${cat.summary}`);
|
|
23502
|
+
for (const item of cat.items) {
|
|
23503
|
+
lines.push(` ${chalk17.dim("\u2514")} ${item.id} ${chalk17.dim(item.detail)}`);
|
|
23504
|
+
}
|
|
23505
|
+
}
|
|
23506
|
+
lines.push("");
|
|
23507
|
+
lines.push(` ${SEPARATOR2}`);
|
|
23508
|
+
lines.push(chalk17.bold(" Process"));
|
|
23509
|
+
lines.push(` ${SEPARATOR2}`);
|
|
23510
|
+
for (const cat of report.process) {
|
|
23511
|
+
lines.push(` ${STATUS_DOT2[cat.status]} ${chalk17.bold(cat.name.padEnd(22))} ${cat.summary}`);
|
|
23512
|
+
for (const item of cat.items) {
|
|
23513
|
+
lines.push(` ${chalk17.dim("\u2514")} ${item.id} ${chalk17.dim(item.detail)}`);
|
|
23514
|
+
}
|
|
23515
|
+
}
|
|
23516
|
+
lines.push(` ${SEPARATOR2}`);
|
|
23517
|
+
lines.push("");
|
|
23518
|
+
return lines.join("\n");
|
|
23519
|
+
}
|
|
23520
|
+
|
|
23521
|
+
// src/reports/health/render-confluence.ts
|
|
23522
|
+
var EMOJI2 = {
|
|
23523
|
+
green: ":green_circle:",
|
|
23524
|
+
amber: ":yellow_circle:",
|
|
23525
|
+
red: ":red_circle:"
|
|
23526
|
+
};
|
|
23527
|
+
function renderConfluence2(report) {
|
|
23528
|
+
const lines = [];
|
|
23529
|
+
lines.push(`# Health Check \u2014 ${report.projectName}`);
|
|
23530
|
+
lines.push("");
|
|
23531
|
+
lines.push(`**Date:** ${report.generatedAt}`);
|
|
23532
|
+
lines.push(`**Overall:** ${EMOJI2[report.overall]} ${report.overall.toUpperCase()}`);
|
|
23533
|
+
lines.push("");
|
|
23534
|
+
lines.push("## Completeness");
|
|
23535
|
+
lines.push("");
|
|
23536
|
+
lines.push("| Category | Status | Summary |");
|
|
23537
|
+
lines.push("|----------|--------|---------|");
|
|
23538
|
+
for (const cat of report.completeness) {
|
|
23539
|
+
lines.push(
|
|
23540
|
+
`| ${cat.name} | ${EMOJI2[cat.status]} ${cat.status.toUpperCase()} | ${cat.summary} |`
|
|
23541
|
+
);
|
|
23542
|
+
}
|
|
23543
|
+
lines.push("");
|
|
23544
|
+
for (const cat of report.completeness) {
|
|
23545
|
+
if (cat.items.length === 0) continue;
|
|
23546
|
+
lines.push(`### ${cat.name}`);
|
|
23547
|
+
lines.push("");
|
|
23548
|
+
for (const item of cat.items) {
|
|
23549
|
+
lines.push(`- **${item.id}** ${item.detail}`);
|
|
23550
|
+
}
|
|
23551
|
+
lines.push("");
|
|
23552
|
+
}
|
|
23553
|
+
lines.push("## Process");
|
|
23554
|
+
lines.push("");
|
|
23555
|
+
lines.push("| Metric | Status | Summary |");
|
|
23556
|
+
lines.push("|--------|--------|---------|");
|
|
23557
|
+
for (const cat of report.process) {
|
|
23558
|
+
lines.push(
|
|
23559
|
+
`| ${cat.name} | ${EMOJI2[cat.status]} ${cat.status.toUpperCase()} | ${cat.summary} |`
|
|
23560
|
+
);
|
|
23561
|
+
}
|
|
23562
|
+
lines.push("");
|
|
23563
|
+
for (const cat of report.process) {
|
|
23564
|
+
if (cat.items.length === 0) continue;
|
|
23565
|
+
lines.push(`### ${cat.name}`);
|
|
23566
|
+
lines.push("");
|
|
23567
|
+
for (const item of cat.items) {
|
|
23568
|
+
lines.push(`- **${item.id}** ${item.detail}`);
|
|
23569
|
+
}
|
|
23570
|
+
lines.push("");
|
|
23571
|
+
}
|
|
23572
|
+
return lines.join("\n");
|
|
23573
|
+
}
|
|
23574
|
+
|
|
22636
23575
|
// src/cli/commands/report.ts
|
|
22637
23576
|
async function garReportCommand(options) {
|
|
22638
23577
|
const project = loadProject();
|
|
@@ -22648,6 +23587,20 @@ async function garReportCommand(options) {
|
|
|
22648
23587
|
console.log(renderAscii(report));
|
|
22649
23588
|
}
|
|
22650
23589
|
}
|
|
23590
|
+
async function healthReportCommand(options) {
|
|
23591
|
+
const project = loadProject();
|
|
23592
|
+
const plugin = resolvePlugin(project.config.methodology);
|
|
23593
|
+
const registrations = plugin?.documentTypeRegistrations ?? [];
|
|
23594
|
+
const store = new DocumentStore(project.marvinDir, registrations);
|
|
23595
|
+
const metrics = collectHealthMetrics(store);
|
|
23596
|
+
const report = evaluateHealth(project.config.name, metrics);
|
|
23597
|
+
const format = options.format ?? "ascii";
|
|
23598
|
+
if (format === "confluence") {
|
|
23599
|
+
console.log(renderConfluence2(report));
|
|
23600
|
+
} else {
|
|
23601
|
+
console.log(renderAscii2(report));
|
|
23602
|
+
}
|
|
23603
|
+
}
|
|
22651
23604
|
|
|
22652
23605
|
// src/cli/commands/web.ts
|
|
22653
23606
|
async function webCommand(options) {
|
|
@@ -22659,12 +23612,38 @@ async function webCommand(options) {
|
|
|
22659
23612
|
await startWebServer({ port, open: options.open });
|
|
22660
23613
|
}
|
|
22661
23614
|
|
|
23615
|
+
// src/cli/commands/generate.ts
|
|
23616
|
+
import * as fs18 from "fs";
|
|
23617
|
+
import * as path19 from "path";
|
|
23618
|
+
import chalk18 from "chalk";
|
|
23619
|
+
import { confirm as confirm2 } from "@inquirer/prompts";
|
|
23620
|
+
async function generateClaudeMdCommand(options) {
|
|
23621
|
+
const project = loadProject();
|
|
23622
|
+
const filePath = path19.join(project.marvinDir, "CLAUDE.md");
|
|
23623
|
+
if (fs18.existsSync(filePath) && !options.force) {
|
|
23624
|
+
const overwrite = await confirm2({
|
|
23625
|
+
message: ".marvin/CLAUDE.md already exists. Overwrite?",
|
|
23626
|
+
default: false
|
|
23627
|
+
});
|
|
23628
|
+
if (!overwrite) {
|
|
23629
|
+
console.log(chalk18.dim("Aborted."));
|
|
23630
|
+
return;
|
|
23631
|
+
}
|
|
23632
|
+
}
|
|
23633
|
+
fs18.writeFileSync(
|
|
23634
|
+
filePath,
|
|
23635
|
+
getDefaultClaudeMdContent(project.config.name),
|
|
23636
|
+
"utf-8"
|
|
23637
|
+
);
|
|
23638
|
+
console.log(chalk18.green("Created .marvin/CLAUDE.md"));
|
|
23639
|
+
}
|
|
23640
|
+
|
|
22662
23641
|
// src/cli/program.ts
|
|
22663
23642
|
function createProgram() {
|
|
22664
23643
|
const program = new Command();
|
|
22665
23644
|
program.name("marvin").description(
|
|
22666
23645
|
"AI-powered product development assistant with Product Owner, Delivery Manager, and Technical Lead personas"
|
|
22667
|
-
).version("0.3.
|
|
23646
|
+
).version("0.3.6");
|
|
22668
23647
|
program.command("init").description("Initialize a new Marvin project in the current directory").action(async () => {
|
|
22669
23648
|
await initCommand();
|
|
22670
23649
|
});
|
|
@@ -22741,9 +23720,19 @@ function createProgram() {
|
|
|
22741
23720
|
).action(async (options) => {
|
|
22742
23721
|
await garReportCommand(options);
|
|
22743
23722
|
});
|
|
23723
|
+
reportCmd.command("health").description("Generate a governance health check report").option(
|
|
23724
|
+
"--format <format>",
|
|
23725
|
+
"Output format: ascii or confluence (default: ascii)"
|
|
23726
|
+
).action(async (options) => {
|
|
23727
|
+
await healthReportCommand(options);
|
|
23728
|
+
});
|
|
22744
23729
|
program.command("web").description("Launch a local web dashboard for project data").option("-p, --port <port>", "Port to listen on (default: 3000)").option("--no-open", "Don't auto-open the browser").action(async (options) => {
|
|
22745
23730
|
await webCommand(options);
|
|
22746
23731
|
});
|
|
23732
|
+
const generateCmd = program.command("generate").description("Generate project files");
|
|
23733
|
+
generateCmd.command("claude-md").description("Generate .marvin/CLAUDE.md project instruction file").option("--force", "Overwrite existing file without prompting").action(async (options) => {
|
|
23734
|
+
await generateClaudeMdCommand(options);
|
|
23735
|
+
});
|
|
22747
23736
|
return program;
|
|
22748
23737
|
}
|
|
22749
23738
|
export {
|