mrvn-cli 0.3.3 → 0.3.5
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 +1 -0
- package/dist/index.js +956 -85
- package/dist/index.js.map +1 -1
- package/dist/marvin-serve.js +835 -83
- package/dist/marvin-serve.js.map +1 -1
- package/dist/marvin.js +956 -85
- 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
|
|
@@ -536,7 +538,7 @@ function buildSystemPrompt(persona, projectConfig, pluginPromptFragment, skillPr
|
|
|
536
538
|
## Available Tools
|
|
537
539
|
You have access to governance tools for managing project artifacts:
|
|
538
540
|
- **Decisions** (D-xxx): List, get, create, and update decisions
|
|
539
|
-
- **Actions** (A-xxx): List, get, create, and update action items
|
|
541
|
+
- **Actions** (A-xxx): List, get, create, and update action items. Actions support \`dueDate\` for schedule tracking and \`sprints\` for sprint assignment.
|
|
540
542
|
- **Questions** (Q-xxx): List, get, create, and update questions
|
|
541
543
|
- **Features** (F-xxx): List, get, create, and update feature definitions
|
|
542
544
|
- **Epics** (E-xxx): List, get, create, and update implementation epics (must link to approved features)
|
|
@@ -14365,7 +14367,7 @@ function createDecisionTools(store) {
|
|
|
14365
14367
|
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
14366
14368
|
};
|
|
14367
14369
|
},
|
|
14368
|
-
{ annotations: {
|
|
14370
|
+
{ annotations: { readOnlyHint: true } }
|
|
14369
14371
|
),
|
|
14370
14372
|
tool(
|
|
14371
14373
|
"get_decision",
|
|
@@ -14392,7 +14394,7 @@ function createDecisionTools(store) {
|
|
|
14392
14394
|
]
|
|
14393
14395
|
};
|
|
14394
14396
|
},
|
|
14395
|
-
{ annotations: {
|
|
14397
|
+
{ annotations: { readOnlyHint: true } }
|
|
14396
14398
|
),
|
|
14397
14399
|
tool(
|
|
14398
14400
|
"create_decision",
|
|
@@ -14453,6 +14455,19 @@ function createDecisionTools(store) {
|
|
|
14453
14455
|
|
|
14454
14456
|
// src/agent/tools/actions.ts
|
|
14455
14457
|
import { tool as tool2 } from "@anthropic-ai/claude-agent-sdk";
|
|
14458
|
+
function findMatchingSprints(store, dueDate) {
|
|
14459
|
+
const sprints = store.list({ type: "sprint" });
|
|
14460
|
+
return sprints.filter((s) => {
|
|
14461
|
+
const start = s.frontmatter.startDate;
|
|
14462
|
+
const end = s.frontmatter.endDate;
|
|
14463
|
+
return start && end && dueDate >= start && dueDate <= end;
|
|
14464
|
+
}).map((s) => ({
|
|
14465
|
+
id: s.frontmatter.id,
|
|
14466
|
+
title: s.frontmatter.title,
|
|
14467
|
+
startDate: s.frontmatter.startDate,
|
|
14468
|
+
endDate: s.frontmatter.endDate
|
|
14469
|
+
}));
|
|
14470
|
+
}
|
|
14456
14471
|
function createActionTools(store) {
|
|
14457
14472
|
return [
|
|
14458
14473
|
tool2(
|
|
@@ -14468,19 +14483,24 @@ function createActionTools(store) {
|
|
|
14468
14483
|
status: args.status,
|
|
14469
14484
|
owner: args.owner
|
|
14470
14485
|
});
|
|
14471
|
-
const summary = docs.map((d) =>
|
|
14472
|
-
|
|
14473
|
-
|
|
14474
|
-
|
|
14475
|
-
|
|
14476
|
-
|
|
14477
|
-
|
|
14478
|
-
|
|
14486
|
+
const summary = docs.map((d) => {
|
|
14487
|
+
const sprintIds = (d.frontmatter.tags ?? []).filter((t) => t.startsWith("sprint:")).map((t) => t.slice(7));
|
|
14488
|
+
return {
|
|
14489
|
+
id: d.frontmatter.id,
|
|
14490
|
+
title: d.frontmatter.title,
|
|
14491
|
+
status: d.frontmatter.status,
|
|
14492
|
+
owner: d.frontmatter.owner,
|
|
14493
|
+
priority: d.frontmatter.priority,
|
|
14494
|
+
dueDate: d.frontmatter.dueDate,
|
|
14495
|
+
sprints: sprintIds.length > 0 ? sprintIds : void 0,
|
|
14496
|
+
created: d.frontmatter.created
|
|
14497
|
+
};
|
|
14498
|
+
});
|
|
14479
14499
|
return {
|
|
14480
14500
|
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
14481
14501
|
};
|
|
14482
14502
|
},
|
|
14483
|
-
{ annotations: {
|
|
14503
|
+
{ annotations: { readOnlyHint: true } }
|
|
14484
14504
|
),
|
|
14485
14505
|
tool2(
|
|
14486
14506
|
"get_action",
|
|
@@ -14507,7 +14527,7 @@ function createActionTools(store) {
|
|
|
14507
14527
|
]
|
|
14508
14528
|
};
|
|
14509
14529
|
},
|
|
14510
|
-
{ annotations: {
|
|
14530
|
+
{ annotations: { readOnlyHint: true } }
|
|
14511
14531
|
),
|
|
14512
14532
|
tool2(
|
|
14513
14533
|
"create_action",
|
|
@@ -14518,9 +14538,18 @@ function createActionTools(store) {
|
|
|
14518
14538
|
status: external_exports.string().optional().describe("Status (default: 'open')"),
|
|
14519
14539
|
owner: external_exports.string().optional().describe("Person responsible"),
|
|
14520
14540
|
priority: external_exports.string().optional().describe("Priority (high, medium, low)"),
|
|
14521
|
-
tags: external_exports.array(external_exports.string()).optional().describe("Tags for categorization")
|
|
14541
|
+
tags: external_exports.array(external_exports.string()).optional().describe("Tags for categorization"),
|
|
14542
|
+
dueDate: external_exports.string().optional().describe("Due date in ISO format (e.g. '2026-03-15')"),
|
|
14543
|
+
sprints: external_exports.array(external_exports.string()).optional().describe("Sprint IDs to assign (e.g. ['SP-001']). Adds sprint:SP-xxx tags.")
|
|
14522
14544
|
},
|
|
14523
14545
|
async (args) => {
|
|
14546
|
+
const tags = [...args.tags ?? []];
|
|
14547
|
+
if (args.sprints) {
|
|
14548
|
+
for (const sprintId of args.sprints) {
|
|
14549
|
+
const tag = `sprint:${sprintId}`;
|
|
14550
|
+
if (!tags.includes(tag)) tags.push(tag);
|
|
14551
|
+
}
|
|
14552
|
+
}
|
|
14524
14553
|
const doc = store.create(
|
|
14525
14554
|
"action",
|
|
14526
14555
|
{
|
|
@@ -14528,17 +14557,21 @@ function createActionTools(store) {
|
|
|
14528
14557
|
status: args.status,
|
|
14529
14558
|
owner: args.owner,
|
|
14530
14559
|
priority: args.priority,
|
|
14531
|
-
tags:
|
|
14560
|
+
tags: tags.length > 0 ? tags : void 0,
|
|
14561
|
+
dueDate: args.dueDate
|
|
14532
14562
|
},
|
|
14533
14563
|
args.content
|
|
14534
14564
|
);
|
|
14565
|
+
const parts = [`Created action ${doc.frontmatter.id}: ${doc.frontmatter.title}`];
|
|
14566
|
+
if (args.dueDate && (!args.sprints || args.sprints.length === 0)) {
|
|
14567
|
+
const matching = findMatchingSprints(store, args.dueDate);
|
|
14568
|
+
if (matching.length > 0) {
|
|
14569
|
+
const suggestions = matching.map((s) => `${s.id} "${s.title}" (${s.startDate} \u2013 ${s.endDate})`).join(", ");
|
|
14570
|
+
parts.push(`Suggested sprints for dueDate ${args.dueDate}: ${suggestions}. Use the sprints parameter or update_action to assign.`);
|
|
14571
|
+
}
|
|
14572
|
+
}
|
|
14535
14573
|
return {
|
|
14536
|
-
content: [
|
|
14537
|
-
{
|
|
14538
|
-
type: "text",
|
|
14539
|
-
text: `Created action ${doc.frontmatter.id}: ${doc.frontmatter.title}`
|
|
14540
|
-
}
|
|
14541
|
-
]
|
|
14574
|
+
content: [{ type: "text", text: parts.join("\n") }]
|
|
14542
14575
|
};
|
|
14543
14576
|
}
|
|
14544
14577
|
),
|
|
@@ -14551,10 +14584,25 @@ function createActionTools(store) {
|
|
|
14551
14584
|
status: external_exports.string().optional().describe("New status"),
|
|
14552
14585
|
content: external_exports.string().optional().describe("New content"),
|
|
14553
14586
|
owner: external_exports.string().optional().describe("New owner"),
|
|
14554
|
-
priority: external_exports.string().optional().describe("New priority")
|
|
14587
|
+
priority: external_exports.string().optional().describe("New priority"),
|
|
14588
|
+
dueDate: external_exports.string().optional().describe("Due date in ISO format (e.g. '2026-03-15')"),
|
|
14589
|
+
sprints: external_exports.array(external_exports.string()).optional().describe("Sprint IDs to assign (replaces existing sprint tags). E.g. ['SP-001'].")
|
|
14555
14590
|
},
|
|
14556
14591
|
async (args) => {
|
|
14557
|
-
const { id, content, ...updates } = args;
|
|
14592
|
+
const { id, content, sprints, ...updates } = args;
|
|
14593
|
+
if (sprints !== void 0) {
|
|
14594
|
+
const existing = store.get(id);
|
|
14595
|
+
if (!existing) {
|
|
14596
|
+
return {
|
|
14597
|
+
content: [{ type: "text", text: `Action ${id} not found` }],
|
|
14598
|
+
isError: true
|
|
14599
|
+
};
|
|
14600
|
+
}
|
|
14601
|
+
const existingTags = existing.frontmatter.tags ?? [];
|
|
14602
|
+
const nonSprintTags = existingTags.filter((t) => !t.startsWith("sprint:"));
|
|
14603
|
+
const newSprintTags = sprints.map((s) => `sprint:${s}`);
|
|
14604
|
+
updates.tags = [...nonSprintTags, ...newSprintTags];
|
|
14605
|
+
}
|
|
14558
14606
|
const doc = store.update(id, updates, content);
|
|
14559
14607
|
return {
|
|
14560
14608
|
content: [
|
|
@@ -14565,6 +14613,35 @@ function createActionTools(store) {
|
|
|
14565
14613
|
]
|
|
14566
14614
|
};
|
|
14567
14615
|
}
|
|
14616
|
+
),
|
|
14617
|
+
tool2(
|
|
14618
|
+
"suggest_sprints_for_action",
|
|
14619
|
+
"Suggest sprints whose date range contains the given due date. Helps assign actions to the right sprint.",
|
|
14620
|
+
{
|
|
14621
|
+
dueDate: external_exports.string().describe("Due date in ISO format (e.g. '2026-03-15')")
|
|
14622
|
+
},
|
|
14623
|
+
async (args) => {
|
|
14624
|
+
const matching = findMatchingSprints(store, args.dueDate);
|
|
14625
|
+
if (matching.length === 0) {
|
|
14626
|
+
return {
|
|
14627
|
+
content: [
|
|
14628
|
+
{
|
|
14629
|
+
type: "text",
|
|
14630
|
+
text: `No sprints found containing dueDate ${args.dueDate}.`
|
|
14631
|
+
}
|
|
14632
|
+
]
|
|
14633
|
+
};
|
|
14634
|
+
}
|
|
14635
|
+
return {
|
|
14636
|
+
content: [
|
|
14637
|
+
{
|
|
14638
|
+
type: "text",
|
|
14639
|
+
text: JSON.stringify(matching, null, 2)
|
|
14640
|
+
}
|
|
14641
|
+
]
|
|
14642
|
+
};
|
|
14643
|
+
},
|
|
14644
|
+
{ annotations: { readOnlyHint: true } }
|
|
14568
14645
|
)
|
|
14569
14646
|
];
|
|
14570
14647
|
}
|
|
@@ -14592,7 +14669,7 @@ function createQuestionTools(store) {
|
|
|
14592
14669
|
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
14593
14670
|
};
|
|
14594
14671
|
},
|
|
14595
|
-
{ annotations: {
|
|
14672
|
+
{ annotations: { readOnlyHint: true } }
|
|
14596
14673
|
),
|
|
14597
14674
|
tool3(
|
|
14598
14675
|
"get_question",
|
|
@@ -14619,7 +14696,7 @@ function createQuestionTools(store) {
|
|
|
14619
14696
|
]
|
|
14620
14697
|
};
|
|
14621
14698
|
},
|
|
14622
|
-
{ annotations: {
|
|
14699
|
+
{ annotations: { readOnlyHint: true } }
|
|
14623
14700
|
),
|
|
14624
14701
|
tool3(
|
|
14625
14702
|
"create_question",
|
|
@@ -14712,7 +14789,7 @@ function createDocumentTools(store) {
|
|
|
14712
14789
|
]
|
|
14713
14790
|
};
|
|
14714
14791
|
},
|
|
14715
|
-
{ annotations: {
|
|
14792
|
+
{ annotations: { readOnlyHint: true } }
|
|
14716
14793
|
),
|
|
14717
14794
|
tool4(
|
|
14718
14795
|
"read_document",
|
|
@@ -14739,7 +14816,7 @@ function createDocumentTools(store) {
|
|
|
14739
14816
|
]
|
|
14740
14817
|
};
|
|
14741
14818
|
},
|
|
14742
|
-
{ annotations: {
|
|
14819
|
+
{ annotations: { readOnlyHint: true } }
|
|
14743
14820
|
),
|
|
14744
14821
|
tool4(
|
|
14745
14822
|
"project_summary",
|
|
@@ -14767,7 +14844,7 @@ function createDocumentTools(store) {
|
|
|
14767
14844
|
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
14768
14845
|
};
|
|
14769
14846
|
},
|
|
14770
|
-
{ annotations: {
|
|
14847
|
+
{ annotations: { readOnlyHint: true } }
|
|
14771
14848
|
)
|
|
14772
14849
|
];
|
|
14773
14850
|
}
|
|
@@ -14804,7 +14881,7 @@ function createSourceTools(manifest) {
|
|
|
14804
14881
|
]
|
|
14805
14882
|
};
|
|
14806
14883
|
},
|
|
14807
|
-
{ annotations: {
|
|
14884
|
+
{ annotations: { readOnlyHint: true } }
|
|
14808
14885
|
),
|
|
14809
14886
|
tool5(
|
|
14810
14887
|
"get_source_info",
|
|
@@ -14838,7 +14915,7 @@ function createSourceTools(manifest) {
|
|
|
14838
14915
|
]
|
|
14839
14916
|
};
|
|
14840
14917
|
},
|
|
14841
|
-
{ annotations: {
|
|
14918
|
+
{ annotations: { readOnlyHint: true } }
|
|
14842
14919
|
)
|
|
14843
14920
|
];
|
|
14844
14921
|
}
|
|
@@ -14869,7 +14946,7 @@ function createSessionTools(store) {
|
|
|
14869
14946
|
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
14870
14947
|
};
|
|
14871
14948
|
},
|
|
14872
|
-
{ annotations: {
|
|
14949
|
+
{ annotations: { readOnlyHint: true } }
|
|
14873
14950
|
),
|
|
14874
14951
|
tool6(
|
|
14875
14952
|
"get_session",
|
|
@@ -14887,7 +14964,7 @@ function createSessionTools(store) {
|
|
|
14887
14964
|
content: [{ type: "text", text: JSON.stringify(session, null, 2) }]
|
|
14888
14965
|
};
|
|
14889
14966
|
},
|
|
14890
|
-
{ annotations: {
|
|
14967
|
+
{ annotations: { readOnlyHint: true } }
|
|
14891
14968
|
)
|
|
14892
14969
|
];
|
|
14893
14970
|
}
|
|
@@ -14905,9 +14982,17 @@ function collectGarMetrics(store) {
|
|
|
14905
14982
|
const blockedItems = allDocs.filter(
|
|
14906
14983
|
(d) => d.frontmatter.tags?.includes("blocked")
|
|
14907
14984
|
);
|
|
14908
|
-
const
|
|
14985
|
+
const tagOverdueItems = allDocs.filter(
|
|
14909
14986
|
(d) => d.frontmatter.tags?.includes("overdue")
|
|
14910
14987
|
);
|
|
14988
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
14989
|
+
const dateOverdueActions = openActions.filter((d) => {
|
|
14990
|
+
const dueDate = d.frontmatter.dueDate;
|
|
14991
|
+
return typeof dueDate === "string" && dueDate < today;
|
|
14992
|
+
});
|
|
14993
|
+
const overdueItems = [...tagOverdueItems, ...dateOverdueActions].filter(
|
|
14994
|
+
(d, i, arr) => arr.findIndex((x) => x.frontmatter.id === d.frontmatter.id) === i
|
|
14995
|
+
);
|
|
14911
14996
|
const openQuestions = store.list({ type: "question", status: "open" });
|
|
14912
14997
|
const riskItems = allDocs.filter(
|
|
14913
14998
|
(d) => d.frontmatter.tags?.includes("risk")
|
|
@@ -15016,6 +15101,253 @@ function evaluateGar(projectName, metrics) {
|
|
|
15016
15101
|
};
|
|
15017
15102
|
}
|
|
15018
15103
|
|
|
15104
|
+
// src/reports/health/collector.ts
|
|
15105
|
+
var FIELD_CHECKS = [
|
|
15106
|
+
{
|
|
15107
|
+
type: "action",
|
|
15108
|
+
openStatuses: ["open", "in-progress"],
|
|
15109
|
+
requiredFields: ["owner", "priority", "dueDate", "content"]
|
|
15110
|
+
},
|
|
15111
|
+
{
|
|
15112
|
+
type: "decision",
|
|
15113
|
+
openStatuses: ["open", "proposed"],
|
|
15114
|
+
requiredFields: ["owner", "content"]
|
|
15115
|
+
},
|
|
15116
|
+
{
|
|
15117
|
+
type: "question",
|
|
15118
|
+
openStatuses: ["open"],
|
|
15119
|
+
requiredFields: ["owner", "content"]
|
|
15120
|
+
},
|
|
15121
|
+
{
|
|
15122
|
+
type: "feature",
|
|
15123
|
+
openStatuses: ["draft", "approved"],
|
|
15124
|
+
requiredFields: ["owner", "priority", "content"]
|
|
15125
|
+
},
|
|
15126
|
+
{
|
|
15127
|
+
type: "epic",
|
|
15128
|
+
openStatuses: ["planned", "in-progress"],
|
|
15129
|
+
requiredFields: ["owner", "targetDate", "estimatedEffort", "content"]
|
|
15130
|
+
},
|
|
15131
|
+
{
|
|
15132
|
+
type: "sprint",
|
|
15133
|
+
openStatuses: ["planned", "active"],
|
|
15134
|
+
requiredFields: ["goal", "startDate", "endDate", "linkedEpics"]
|
|
15135
|
+
}
|
|
15136
|
+
];
|
|
15137
|
+
var STALE_THRESHOLD_DAYS = 14;
|
|
15138
|
+
var AGING_THRESHOLD_DAYS = 30;
|
|
15139
|
+
function daysBetween(a, b) {
|
|
15140
|
+
const msPerDay = 864e5;
|
|
15141
|
+
const dateA = new Date(a);
|
|
15142
|
+
const dateB = new Date(b);
|
|
15143
|
+
return Math.floor(Math.abs(dateB.getTime() - dateA.getTime()) / msPerDay);
|
|
15144
|
+
}
|
|
15145
|
+
function checkMissingFields(doc, requiredFields) {
|
|
15146
|
+
const missing = [];
|
|
15147
|
+
for (const field of requiredFields) {
|
|
15148
|
+
if (field === "content") {
|
|
15149
|
+
if (!doc.content || doc.content.trim().length === 0) {
|
|
15150
|
+
missing.push("content");
|
|
15151
|
+
}
|
|
15152
|
+
} else if (field === "linkedEpics") {
|
|
15153
|
+
const val = doc.frontmatter[field];
|
|
15154
|
+
if (!Array.isArray(val) || val.length === 0) {
|
|
15155
|
+
missing.push(field);
|
|
15156
|
+
}
|
|
15157
|
+
} else {
|
|
15158
|
+
const val = doc.frontmatter[field];
|
|
15159
|
+
if (val === void 0 || val === null || val === "") {
|
|
15160
|
+
missing.push(field);
|
|
15161
|
+
}
|
|
15162
|
+
}
|
|
15163
|
+
}
|
|
15164
|
+
return missing;
|
|
15165
|
+
}
|
|
15166
|
+
function collectCompleteness(store) {
|
|
15167
|
+
const result = {};
|
|
15168
|
+
for (const check2 of FIELD_CHECKS) {
|
|
15169
|
+
const allOfType = store.list({ type: check2.type });
|
|
15170
|
+
const openDocs = allOfType.filter(
|
|
15171
|
+
(d) => check2.openStatuses.includes(d.frontmatter.status)
|
|
15172
|
+
);
|
|
15173
|
+
const gaps = [];
|
|
15174
|
+
let complete = 0;
|
|
15175
|
+
for (const doc of openDocs) {
|
|
15176
|
+
const missingFields = checkMissingFields(doc, check2.requiredFields);
|
|
15177
|
+
if (missingFields.length === 0) {
|
|
15178
|
+
complete++;
|
|
15179
|
+
} else {
|
|
15180
|
+
gaps.push({
|
|
15181
|
+
id: doc.frontmatter.id,
|
|
15182
|
+
title: doc.frontmatter.title,
|
|
15183
|
+
missingFields
|
|
15184
|
+
});
|
|
15185
|
+
}
|
|
15186
|
+
}
|
|
15187
|
+
result[check2.type] = {
|
|
15188
|
+
total: openDocs.length,
|
|
15189
|
+
complete,
|
|
15190
|
+
gaps
|
|
15191
|
+
};
|
|
15192
|
+
}
|
|
15193
|
+
return result;
|
|
15194
|
+
}
|
|
15195
|
+
function collectProcess(store) {
|
|
15196
|
+
const today = (/* @__PURE__ */ new Date()).toISOString();
|
|
15197
|
+
const allDocs = store.list();
|
|
15198
|
+
const openStatuses = new Set(FIELD_CHECKS.flatMap((c) => c.openStatuses));
|
|
15199
|
+
const openDocs = allDocs.filter((d) => openStatuses.has(d.frontmatter.status));
|
|
15200
|
+
const stale = [];
|
|
15201
|
+
for (const doc of openDocs) {
|
|
15202
|
+
const updated = doc.frontmatter.updated ?? doc.frontmatter.created;
|
|
15203
|
+
const days = daysBetween(updated, today);
|
|
15204
|
+
if (days >= STALE_THRESHOLD_DAYS) {
|
|
15205
|
+
stale.push({ id: doc.frontmatter.id, title: doc.frontmatter.title, days });
|
|
15206
|
+
}
|
|
15207
|
+
}
|
|
15208
|
+
const openActions = store.list({ type: "action" }).filter((d) => d.frontmatter.status === "open" || d.frontmatter.status === "in-progress");
|
|
15209
|
+
const agingActions = [];
|
|
15210
|
+
for (const doc of openActions) {
|
|
15211
|
+
const days = daysBetween(doc.frontmatter.created, today);
|
|
15212
|
+
if (days >= AGING_THRESHOLD_DAYS) {
|
|
15213
|
+
agingActions.push({ id: doc.frontmatter.id, title: doc.frontmatter.title, days });
|
|
15214
|
+
}
|
|
15215
|
+
}
|
|
15216
|
+
const resolvedDecisions = store.list({ type: "decision" }).filter((d) => !["open", "proposed"].includes(d.frontmatter.status));
|
|
15217
|
+
let decisionTotal = 0;
|
|
15218
|
+
for (const doc of resolvedDecisions) {
|
|
15219
|
+
decisionTotal += daysBetween(doc.frontmatter.created, doc.frontmatter.updated);
|
|
15220
|
+
}
|
|
15221
|
+
const decisionVelocity = {
|
|
15222
|
+
avgDays: resolvedDecisions.length > 0 ? Math.round(decisionTotal / resolvedDecisions.length) : 0,
|
|
15223
|
+
count: resolvedDecisions.length
|
|
15224
|
+
};
|
|
15225
|
+
const answeredQuestions = store.list({ type: "question" }).filter((d) => d.frontmatter.status !== "open");
|
|
15226
|
+
let questionTotal = 0;
|
|
15227
|
+
for (const doc of answeredQuestions) {
|
|
15228
|
+
questionTotal += daysBetween(doc.frontmatter.created, doc.frontmatter.updated);
|
|
15229
|
+
}
|
|
15230
|
+
const questionResolution = {
|
|
15231
|
+
avgDays: answeredQuestions.length > 0 ? Math.round(questionTotal / answeredQuestions.length) : 0,
|
|
15232
|
+
count: answeredQuestions.length
|
|
15233
|
+
};
|
|
15234
|
+
return { stale, agingActions, decisionVelocity, questionResolution };
|
|
15235
|
+
}
|
|
15236
|
+
function collectHealthMetrics(store) {
|
|
15237
|
+
return {
|
|
15238
|
+
completeness: collectCompleteness(store),
|
|
15239
|
+
process: collectProcess(store)
|
|
15240
|
+
};
|
|
15241
|
+
}
|
|
15242
|
+
|
|
15243
|
+
// src/reports/health/evaluator.ts
|
|
15244
|
+
function worstStatus2(statuses) {
|
|
15245
|
+
if (statuses.includes("red")) return "red";
|
|
15246
|
+
if (statuses.includes("amber")) return "amber";
|
|
15247
|
+
return "green";
|
|
15248
|
+
}
|
|
15249
|
+
function completenessStatus(total, complete) {
|
|
15250
|
+
if (total === 0) return "green";
|
|
15251
|
+
const pct = Math.round(complete / total * 100);
|
|
15252
|
+
if (pct >= 100) return "green";
|
|
15253
|
+
if (pct >= 75) return "amber";
|
|
15254
|
+
return "red";
|
|
15255
|
+
}
|
|
15256
|
+
var TYPE_LABELS = {
|
|
15257
|
+
action: "Actions",
|
|
15258
|
+
decision: "Decisions",
|
|
15259
|
+
question: "Questions",
|
|
15260
|
+
feature: "Features",
|
|
15261
|
+
epic: "Epics",
|
|
15262
|
+
sprint: "Sprints"
|
|
15263
|
+
};
|
|
15264
|
+
function evaluateHealth(projectName, metrics) {
|
|
15265
|
+
const completeness = [];
|
|
15266
|
+
for (const [type, catMetrics] of Object.entries(metrics.completeness)) {
|
|
15267
|
+
const { total, complete, gaps } = catMetrics;
|
|
15268
|
+
const status = completenessStatus(total, complete);
|
|
15269
|
+
const pct = total > 0 ? Math.round(complete / total * 100) : 100;
|
|
15270
|
+
completeness.push({
|
|
15271
|
+
name: TYPE_LABELS[type] ?? type,
|
|
15272
|
+
status,
|
|
15273
|
+
summary: `${pct}% complete (${complete}/${total})`,
|
|
15274
|
+
items: gaps.map((g) => ({
|
|
15275
|
+
id: g.id,
|
|
15276
|
+
detail: `missing: ${g.missingFields.join(", ")}`
|
|
15277
|
+
}))
|
|
15278
|
+
});
|
|
15279
|
+
}
|
|
15280
|
+
const process3 = [];
|
|
15281
|
+
const staleCount = metrics.process.stale.length;
|
|
15282
|
+
const staleStatus = staleCount === 0 ? "green" : staleCount <= 3 ? "amber" : "red";
|
|
15283
|
+
process3.push({
|
|
15284
|
+
name: "Stale Items",
|
|
15285
|
+
status: staleStatus,
|
|
15286
|
+
summary: staleCount === 0 ? "no stale items" : `${staleCount} item(s) not updated in 14+ days`,
|
|
15287
|
+
items: metrics.process.stale.map((s) => ({
|
|
15288
|
+
id: s.id,
|
|
15289
|
+
detail: `${s.days} days since last update`
|
|
15290
|
+
}))
|
|
15291
|
+
});
|
|
15292
|
+
const agingCount = metrics.process.agingActions.length;
|
|
15293
|
+
const agingStatus = agingCount === 0 ? "green" : agingCount <= 3 ? "amber" : "red";
|
|
15294
|
+
process3.push({
|
|
15295
|
+
name: "Aging Actions",
|
|
15296
|
+
status: agingStatus,
|
|
15297
|
+
summary: agingCount === 0 ? "no aging actions" : `${agingCount} action(s) open for 30+ days`,
|
|
15298
|
+
items: metrics.process.agingActions.map((a) => ({
|
|
15299
|
+
id: a.id,
|
|
15300
|
+
detail: `open for ${a.days} days`
|
|
15301
|
+
}))
|
|
15302
|
+
});
|
|
15303
|
+
const dv = metrics.process.decisionVelocity;
|
|
15304
|
+
let dvStatus;
|
|
15305
|
+
if (dv.count === 0) {
|
|
15306
|
+
dvStatus = "green";
|
|
15307
|
+
} else if (dv.avgDays <= 7) {
|
|
15308
|
+
dvStatus = "green";
|
|
15309
|
+
} else if (dv.avgDays <= 21) {
|
|
15310
|
+
dvStatus = "amber";
|
|
15311
|
+
} else {
|
|
15312
|
+
dvStatus = "red";
|
|
15313
|
+
}
|
|
15314
|
+
process3.push({
|
|
15315
|
+
name: "Decision Velocity",
|
|
15316
|
+
status: dvStatus,
|
|
15317
|
+
summary: dv.count === 0 ? "no resolved decisions" : `avg ${dv.avgDays} days to resolve (${dv.count} decision(s))`,
|
|
15318
|
+
items: []
|
|
15319
|
+
});
|
|
15320
|
+
const qr = metrics.process.questionResolution;
|
|
15321
|
+
let qrStatus;
|
|
15322
|
+
if (qr.count === 0) {
|
|
15323
|
+
qrStatus = "green";
|
|
15324
|
+
} else if (qr.avgDays <= 7) {
|
|
15325
|
+
qrStatus = "green";
|
|
15326
|
+
} else if (qr.avgDays <= 14) {
|
|
15327
|
+
qrStatus = "amber";
|
|
15328
|
+
} else {
|
|
15329
|
+
qrStatus = "red";
|
|
15330
|
+
}
|
|
15331
|
+
process3.push({
|
|
15332
|
+
name: "Question Resolution",
|
|
15333
|
+
status: qrStatus,
|
|
15334
|
+
summary: qr.count === 0 ? "no answered questions" : `avg ${qr.avgDays} days to answer (${qr.count} question(s))`,
|
|
15335
|
+
items: []
|
|
15336
|
+
});
|
|
15337
|
+
const allStatuses = [
|
|
15338
|
+
...completeness.map((c) => c.status),
|
|
15339
|
+
...process3.map((p) => p.status)
|
|
15340
|
+
];
|
|
15341
|
+
const overall = worstStatus2(allStatuses);
|
|
15342
|
+
return {
|
|
15343
|
+
projectName,
|
|
15344
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
15345
|
+
overall,
|
|
15346
|
+
completeness,
|
|
15347
|
+
process: process3
|
|
15348
|
+
};
|
|
15349
|
+
}
|
|
15350
|
+
|
|
15019
15351
|
// src/web/data.ts
|
|
15020
15352
|
function getOverviewData(store) {
|
|
15021
15353
|
const types = [];
|
|
@@ -15087,6 +15419,46 @@ function getBoardData(store, type) {
|
|
|
15087
15419
|
}));
|
|
15088
15420
|
return { columns, type, types };
|
|
15089
15421
|
}
|
|
15422
|
+
function getDiagramData(store) {
|
|
15423
|
+
const allDocs = store.list();
|
|
15424
|
+
const sprints = [];
|
|
15425
|
+
const epics = [];
|
|
15426
|
+
const features = [];
|
|
15427
|
+
const statusCounts = {};
|
|
15428
|
+
for (const doc of allDocs) {
|
|
15429
|
+
const fm = doc.frontmatter;
|
|
15430
|
+
const status = fm.status.toLowerCase();
|
|
15431
|
+
statusCounts[status] = (statusCounts[status] ?? 0) + 1;
|
|
15432
|
+
switch (fm.type) {
|
|
15433
|
+
case "sprint":
|
|
15434
|
+
sprints.push({
|
|
15435
|
+
id: fm.id,
|
|
15436
|
+
title: fm.title,
|
|
15437
|
+
status: fm.status,
|
|
15438
|
+
startDate: fm.startDate,
|
|
15439
|
+
endDate: fm.endDate,
|
|
15440
|
+
linkedEpics: fm.linkedEpics ?? []
|
|
15441
|
+
});
|
|
15442
|
+
break;
|
|
15443
|
+
case "epic":
|
|
15444
|
+
epics.push({
|
|
15445
|
+
id: fm.id,
|
|
15446
|
+
title: fm.title,
|
|
15447
|
+
status: fm.status,
|
|
15448
|
+
linkedFeature: fm.linkedFeature
|
|
15449
|
+
});
|
|
15450
|
+
break;
|
|
15451
|
+
case "feature":
|
|
15452
|
+
features.push({
|
|
15453
|
+
id: fm.id,
|
|
15454
|
+
title: fm.title,
|
|
15455
|
+
status: fm.status
|
|
15456
|
+
});
|
|
15457
|
+
break;
|
|
15458
|
+
}
|
|
15459
|
+
}
|
|
15460
|
+
return { sprints, epics, features, statusCounts };
|
|
15461
|
+
}
|
|
15090
15462
|
|
|
15091
15463
|
// src/web/templates/layout.ts
|
|
15092
15464
|
function escapeHtml(str) {
|
|
@@ -15117,16 +15489,43 @@ function renderMarkdown(md) {
|
|
|
15117
15489
|
const out = [];
|
|
15118
15490
|
let inList = false;
|
|
15119
15491
|
let listTag = "ul";
|
|
15120
|
-
|
|
15121
|
-
|
|
15492
|
+
let inTable = false;
|
|
15493
|
+
let i = 0;
|
|
15494
|
+
while (i < lines.length) {
|
|
15495
|
+
const line = lines[i];
|
|
15122
15496
|
if (inList && !/^\s*[-*]\s/.test(line) && !/^\s*\d+\.\s/.test(line) && line.trim() !== "") {
|
|
15123
15497
|
out.push(`</${listTag}>`);
|
|
15124
15498
|
inList = false;
|
|
15125
15499
|
}
|
|
15500
|
+
if (inTable && !/^\s*\|/.test(line)) {
|
|
15501
|
+
out.push("</tbody></table></div>");
|
|
15502
|
+
inTable = false;
|
|
15503
|
+
}
|
|
15504
|
+
if (/^(-{3,}|\*{3,}|_{3,})\s*$/.test(line.trim())) {
|
|
15505
|
+
i++;
|
|
15506
|
+
out.push("<hr>");
|
|
15507
|
+
continue;
|
|
15508
|
+
}
|
|
15509
|
+
if (!inTable && /^\s*\|/.test(line) && i + 1 < lines.length && /^\s*\|[\s:|-]+\|\s*$/.test(lines[i + 1])) {
|
|
15510
|
+
const headers = parseTableRow(line);
|
|
15511
|
+
out.push('<div class="table-wrap"><table><thead><tr>');
|
|
15512
|
+
out.push(headers.map((h) => `<th>${inline(h)}</th>`).join(""));
|
|
15513
|
+
out.push("</tr></thead><tbody>");
|
|
15514
|
+
inTable = true;
|
|
15515
|
+
i += 2;
|
|
15516
|
+
continue;
|
|
15517
|
+
}
|
|
15518
|
+
if (inTable && /^\s*\|/.test(line)) {
|
|
15519
|
+
const cells = parseTableRow(line);
|
|
15520
|
+
out.push("<tr>" + cells.map((c) => `<td>${inline(c)}</td>`).join("") + "</tr>");
|
|
15521
|
+
i++;
|
|
15522
|
+
continue;
|
|
15523
|
+
}
|
|
15126
15524
|
const headingMatch = line.match(/^(#{1,3})\s+(.+)$/);
|
|
15127
15525
|
if (headingMatch) {
|
|
15128
15526
|
const level = headingMatch[1].length;
|
|
15129
15527
|
out.push(`<h${level}>${inline(headingMatch[2])}</h${level}>`);
|
|
15528
|
+
i++;
|
|
15130
15529
|
continue;
|
|
15131
15530
|
}
|
|
15132
15531
|
const ulMatch = line.match(/^\s*[-*]\s+(.+)$/);
|
|
@@ -15138,6 +15537,7 @@ function renderMarkdown(md) {
|
|
|
15138
15537
|
listTag = "ul";
|
|
15139
15538
|
}
|
|
15140
15539
|
out.push(`<li>${inline(ulMatch[1])}</li>`);
|
|
15540
|
+
i++;
|
|
15141
15541
|
continue;
|
|
15142
15542
|
}
|
|
15143
15543
|
const olMatch = line.match(/^\s*\d+\.\s+(.+)$/);
|
|
@@ -15149,6 +15549,7 @@ function renderMarkdown(md) {
|
|
|
15149
15549
|
listTag = "ol";
|
|
15150
15550
|
}
|
|
15151
15551
|
out.push(`<li>${inline(olMatch[1])}</li>`);
|
|
15552
|
+
i++;
|
|
15152
15553
|
continue;
|
|
15153
15554
|
}
|
|
15154
15555
|
if (line.trim() === "") {
|
|
@@ -15156,13 +15557,19 @@ function renderMarkdown(md) {
|
|
|
15156
15557
|
out.push(`</${listTag}>`);
|
|
15157
15558
|
inList = false;
|
|
15158
15559
|
}
|
|
15560
|
+
i++;
|
|
15159
15561
|
continue;
|
|
15160
15562
|
}
|
|
15161
15563
|
out.push(`<p>${inline(line)}</p>`);
|
|
15564
|
+
i++;
|
|
15162
15565
|
}
|
|
15163
15566
|
if (inList) out.push(`</${listTag}>`);
|
|
15567
|
+
if (inTable) out.push("</tbody></table></div>");
|
|
15164
15568
|
return out.join("\n");
|
|
15165
15569
|
}
|
|
15570
|
+
function parseTableRow(line) {
|
|
15571
|
+
return line.replace(/^\s*\|/, "").replace(/\|\s*$/, "").split("|").map((cell) => cell.trim());
|
|
15572
|
+
}
|
|
15166
15573
|
function inline(text) {
|
|
15167
15574
|
let s = escapeHtml(text);
|
|
15168
15575
|
s = s.replace(/`([^`]+)`/g, "<code>$1</code>");
|
|
@@ -15176,7 +15583,8 @@ function layout(opts, body) {
|
|
|
15176
15583
|
const topItems = [
|
|
15177
15584
|
{ href: "/", label: "Overview" },
|
|
15178
15585
|
{ href: "/board", label: "Board" },
|
|
15179
|
-
{ href: "/gar", label: "GAR Report" }
|
|
15586
|
+
{ href: "/gar", label: "GAR Report" },
|
|
15587
|
+
{ href: "/health", label: "Health" }
|
|
15180
15588
|
];
|
|
15181
15589
|
const isActive = (href) => opts.activePath === href || href !== "/" && opts.activePath.startsWith(href) ? " active" : "";
|
|
15182
15590
|
const groupsHtml = opts.navGroups.map((group) => {
|
|
@@ -15211,9 +15619,15 @@ function layout(opts, body) {
|
|
|
15211
15619
|
</nav>
|
|
15212
15620
|
</aside>
|
|
15213
15621
|
<main class="main">
|
|
15622
|
+
<button class="expand-toggle" onclick="document.querySelector('.main').classList.toggle('expanded')" title="Toggle wide view">
|
|
15623
|
+
<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>
|
|
15624
|
+
<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>
|
|
15625
|
+
</button>
|
|
15214
15626
|
${body}
|
|
15215
15627
|
</main>
|
|
15216
15628
|
</div>
|
|
15629
|
+
<script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
|
|
15630
|
+
<script>mermaid.initialize({ startOnLoad: true, theme: 'dark' });</script>
|
|
15217
15631
|
</body>
|
|
15218
15632
|
</html>`;
|
|
15219
15633
|
}
|
|
@@ -15328,7 +15742,33 @@ a:hover { text-decoration: underline; }
|
|
|
15328
15742
|
flex: 1;
|
|
15329
15743
|
padding: 2rem 2.5rem;
|
|
15330
15744
|
max-width: 1200px;
|
|
15745
|
+
position: relative;
|
|
15746
|
+
transition: max-width 0.2s ease;
|
|
15747
|
+
}
|
|
15748
|
+
.main.expanded {
|
|
15749
|
+
max-width: none;
|
|
15750
|
+
}
|
|
15751
|
+
.expand-toggle {
|
|
15752
|
+
position: absolute;
|
|
15753
|
+
top: 1rem;
|
|
15754
|
+
right: 1rem;
|
|
15755
|
+
background: var(--bg-card);
|
|
15756
|
+
border: 1px solid var(--border);
|
|
15757
|
+
border-radius: var(--radius);
|
|
15758
|
+
color: var(--text-dim);
|
|
15759
|
+
cursor: pointer;
|
|
15760
|
+
padding: 0.4rem;
|
|
15761
|
+
display: flex;
|
|
15762
|
+
align-items: center;
|
|
15763
|
+
justify-content: center;
|
|
15764
|
+
transition: color 0.15s, border-color 0.15s;
|
|
15331
15765
|
}
|
|
15766
|
+
.expand-toggle:hover {
|
|
15767
|
+
color: var(--text);
|
|
15768
|
+
border-color: var(--text-dim);
|
|
15769
|
+
}
|
|
15770
|
+
.main.expanded .icon-expand { display: none; }
|
|
15771
|
+
.main:not(.expanded) .icon-collapse { display: none; }
|
|
15332
15772
|
|
|
15333
15773
|
/* Page header */
|
|
15334
15774
|
.page-header {
|
|
@@ -15357,12 +15797,26 @@ a:hover { text-decoration: underline; }
|
|
|
15357
15797
|
.breadcrumb a:hover { color: var(--accent); }
|
|
15358
15798
|
.breadcrumb .sep { margin: 0 0.4rem; }
|
|
15359
15799
|
|
|
15800
|
+
/* Card groups */
|
|
15801
|
+
.card-group {
|
|
15802
|
+
margin-bottom: 1.5rem;
|
|
15803
|
+
}
|
|
15804
|
+
|
|
15805
|
+
.card-group-label {
|
|
15806
|
+
font-size: 0.7rem;
|
|
15807
|
+
text-transform: uppercase;
|
|
15808
|
+
letter-spacing: 0.08em;
|
|
15809
|
+
color: var(--text-dim);
|
|
15810
|
+
font-weight: 600;
|
|
15811
|
+
margin-bottom: 0.5rem;
|
|
15812
|
+
}
|
|
15813
|
+
|
|
15360
15814
|
/* Cards grid */
|
|
15361
15815
|
.cards {
|
|
15362
15816
|
display: grid;
|
|
15363
15817
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
15364
15818
|
gap: 1rem;
|
|
15365
|
-
margin-bottom:
|
|
15819
|
+
margin-bottom: 0.5rem;
|
|
15366
15820
|
}
|
|
15367
15821
|
|
|
15368
15822
|
.card {
|
|
@@ -15643,6 +16097,14 @@ tr:hover td {
|
|
|
15643
16097
|
font-family: var(--mono);
|
|
15644
16098
|
font-size: 0.85em;
|
|
15645
16099
|
}
|
|
16100
|
+
.detail-content hr {
|
|
16101
|
+
border: none;
|
|
16102
|
+
border-top: 1px solid var(--border);
|
|
16103
|
+
margin: 1.25rem 0;
|
|
16104
|
+
}
|
|
16105
|
+
.detail-content .table-wrap {
|
|
16106
|
+
margin: 0.75rem 0;
|
|
16107
|
+
}
|
|
15646
16108
|
|
|
15647
16109
|
/* Filters */
|
|
15648
16110
|
.filters {
|
|
@@ -15687,21 +16149,206 @@ tr:hover td {
|
|
|
15687
16149
|
.priority-high { color: var(--red); }
|
|
15688
16150
|
.priority-medium { color: var(--amber); }
|
|
15689
16151
|
.priority-low { color: var(--green); }
|
|
16152
|
+
|
|
16153
|
+
/* Health */
|
|
16154
|
+
.health-section-title {
|
|
16155
|
+
font-size: 1.1rem;
|
|
16156
|
+
font-weight: 600;
|
|
16157
|
+
margin: 2rem 0 1rem;
|
|
16158
|
+
color: var(--text);
|
|
16159
|
+
}
|
|
16160
|
+
|
|
16161
|
+
/* Mermaid diagrams */
|
|
16162
|
+
.mermaid-container {
|
|
16163
|
+
background: var(--bg-card);
|
|
16164
|
+
border: 1px solid var(--border);
|
|
16165
|
+
border-radius: var(--radius);
|
|
16166
|
+
padding: 1.5rem;
|
|
16167
|
+
margin: 1rem 0;
|
|
16168
|
+
overflow-x: auto;
|
|
16169
|
+
}
|
|
16170
|
+
|
|
16171
|
+
.mermaid-container .mermaid {
|
|
16172
|
+
display: flex;
|
|
16173
|
+
justify-content: center;
|
|
16174
|
+
}
|
|
16175
|
+
|
|
16176
|
+
.mermaid-empty {
|
|
16177
|
+
text-align: center;
|
|
16178
|
+
color: var(--text-dim);
|
|
16179
|
+
font-size: 0.875rem;
|
|
16180
|
+
}
|
|
16181
|
+
|
|
16182
|
+
.mermaid-row {
|
|
16183
|
+
display: grid;
|
|
16184
|
+
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
|
16185
|
+
gap: 1rem;
|
|
16186
|
+
}
|
|
16187
|
+
|
|
16188
|
+
.mermaid-row .mermaid-container {
|
|
16189
|
+
margin: 0;
|
|
16190
|
+
}
|
|
15690
16191
|
`;
|
|
15691
16192
|
}
|
|
15692
16193
|
|
|
16194
|
+
// src/web/templates/mermaid.ts
|
|
16195
|
+
function sanitize(text, maxLen = 40) {
|
|
16196
|
+
const cleaned = text.replace(/["'`]/g, "").replace(/[\r\n]+/g, " ");
|
|
16197
|
+
return cleaned.length > maxLen ? cleaned.slice(0, maxLen - 1) + "\u2026" : cleaned;
|
|
16198
|
+
}
|
|
16199
|
+
function mermaidBlock(definition) {
|
|
16200
|
+
return `<div class="mermaid-container"><pre class="mermaid">
|
|
16201
|
+
${definition}
|
|
16202
|
+
</pre></div>`;
|
|
16203
|
+
}
|
|
16204
|
+
function placeholder(message) {
|
|
16205
|
+
return `<div class="mermaid-container mermaid-empty"><p>${message}</p></div>`;
|
|
16206
|
+
}
|
|
16207
|
+
function buildTimelineGantt(data) {
|
|
16208
|
+
const sprintsWithDates = data.sprints.filter((s) => s.startDate && s.endDate);
|
|
16209
|
+
if (sprintsWithDates.length === 0) {
|
|
16210
|
+
return placeholder("No timeline data available \u2014 sprints need start and end dates.");
|
|
16211
|
+
}
|
|
16212
|
+
const epicMap = new Map(data.epics.map((e) => [e.id, e]));
|
|
16213
|
+
const lines = ["gantt", " title Project Timeline", " dateFormat YYYY-MM-DD"];
|
|
16214
|
+
for (const sprint of sprintsWithDates) {
|
|
16215
|
+
lines.push(` section ${sanitize(sprint.id + " " + sprint.title, 50)}`);
|
|
16216
|
+
const linked = sprint.linkedEpics.map((eid) => epicMap.get(eid)).filter(Boolean);
|
|
16217
|
+
if (linked.length === 0) {
|
|
16218
|
+
lines.push(` ${sanitize(sprint.title)} :${sprint.startDate}, ${sprint.endDate}`);
|
|
16219
|
+
} else {
|
|
16220
|
+
for (const epic of linked) {
|
|
16221
|
+
const tag = epic.status === "in-progress" ? "active, " : epic.status === "done" ? "done, " : "";
|
|
16222
|
+
lines.push(` ${sanitize(epic.id + " " + epic.title)} :${tag}${sprint.startDate}, ${sprint.endDate}`);
|
|
16223
|
+
}
|
|
16224
|
+
}
|
|
16225
|
+
}
|
|
16226
|
+
return mermaidBlock(lines.join("\n"));
|
|
16227
|
+
}
|
|
16228
|
+
function buildArtifactFlowchart(data) {
|
|
16229
|
+
if (data.features.length === 0 && data.epics.length === 0) {
|
|
16230
|
+
return placeholder("No artifact relationships found \u2014 create features and epics to see the hierarchy.");
|
|
16231
|
+
}
|
|
16232
|
+
const lines = ["graph TD"];
|
|
16233
|
+
lines.push(" classDef done fill:#065f46,stroke:#34d399,color:#d1fae5");
|
|
16234
|
+
lines.push(" classDef inprogress fill:#78350f,stroke:#fbbf24,color:#fef3c7");
|
|
16235
|
+
lines.push(" classDef blocked fill:#7f1d1d,stroke:#f87171,color:#fee2e2");
|
|
16236
|
+
lines.push(" classDef default fill:#1e293b,stroke:#475569,color:#e2e8f0");
|
|
16237
|
+
const nodeIds = /* @__PURE__ */ new Set();
|
|
16238
|
+
for (const epic of data.epics) {
|
|
16239
|
+
if (epic.linkedFeature) {
|
|
16240
|
+
const feature = data.features.find((f) => f.id === epic.linkedFeature);
|
|
16241
|
+
if (feature) {
|
|
16242
|
+
const fNode = feature.id.replace(/-/g, "_");
|
|
16243
|
+
const eNode = epic.id.replace(/-/g, "_");
|
|
16244
|
+
if (!nodeIds.has(fNode)) {
|
|
16245
|
+
lines.push(` ${fNode}["${sanitize(feature.id + " " + feature.title)}"]`);
|
|
16246
|
+
nodeIds.add(fNode);
|
|
16247
|
+
}
|
|
16248
|
+
if (!nodeIds.has(eNode)) {
|
|
16249
|
+
lines.push(` ${eNode}["${sanitize(epic.id + " " + epic.title)}"]`);
|
|
16250
|
+
nodeIds.add(eNode);
|
|
16251
|
+
}
|
|
16252
|
+
lines.push(` ${fNode} --> ${eNode}`);
|
|
16253
|
+
}
|
|
16254
|
+
}
|
|
16255
|
+
}
|
|
16256
|
+
for (const sprint of data.sprints) {
|
|
16257
|
+
const sNode = sprint.id.replace(/-/g, "_");
|
|
16258
|
+
for (const epicId of sprint.linkedEpics) {
|
|
16259
|
+
const epic = data.epics.find((e) => e.id === epicId);
|
|
16260
|
+
if (epic) {
|
|
16261
|
+
const eNode = epic.id.replace(/-/g, "_");
|
|
16262
|
+
if (!nodeIds.has(eNode)) {
|
|
16263
|
+
lines.push(` ${eNode}["${sanitize(epic.id + " " + epic.title)}"]`);
|
|
16264
|
+
nodeIds.add(eNode);
|
|
16265
|
+
}
|
|
16266
|
+
if (!nodeIds.has(sNode)) {
|
|
16267
|
+
lines.push(` ${sNode}["${sanitize(sprint.id + " " + sprint.title)}"]`);
|
|
16268
|
+
nodeIds.add(sNode);
|
|
16269
|
+
}
|
|
16270
|
+
lines.push(` ${eNode} --> ${sNode}`);
|
|
16271
|
+
}
|
|
16272
|
+
}
|
|
16273
|
+
}
|
|
16274
|
+
if (nodeIds.size === 0) {
|
|
16275
|
+
return placeholder("No artifact relationships found \u2014 link epics to features and sprints.");
|
|
16276
|
+
}
|
|
16277
|
+
const allItems = [
|
|
16278
|
+
...data.features.map((f) => ({ id: f.id, status: f.status })),
|
|
16279
|
+
...data.epics.map((e) => ({ id: e.id, status: e.status })),
|
|
16280
|
+
...data.sprints.map((s) => ({ id: s.id, status: s.status }))
|
|
16281
|
+
];
|
|
16282
|
+
for (const item of allItems) {
|
|
16283
|
+
const node = item.id.replace(/-/g, "_");
|
|
16284
|
+
if (!nodeIds.has(node)) continue;
|
|
16285
|
+
const cls = item.status === "done" || item.status === "completed" ? "done" : item.status === "in-progress" || item.status === "active" ? "inprogress" : item.status === "blocked" ? "blocked" : null;
|
|
16286
|
+
if (cls) {
|
|
16287
|
+
lines.push(` class ${node} ${cls}`);
|
|
16288
|
+
}
|
|
16289
|
+
}
|
|
16290
|
+
return mermaidBlock(lines.join("\n"));
|
|
16291
|
+
}
|
|
16292
|
+
function buildStatusPie(title, counts) {
|
|
16293
|
+
const entries = Object.entries(counts).filter(([, v]) => v > 0);
|
|
16294
|
+
if (entries.length === 0) {
|
|
16295
|
+
return placeholder(`No data for ${title}.`);
|
|
16296
|
+
}
|
|
16297
|
+
const lines = [`pie title ${sanitize(title, 60)}`];
|
|
16298
|
+
for (const [label, count] of entries) {
|
|
16299
|
+
lines.push(` "${sanitize(label, 30)}" : ${count}`);
|
|
16300
|
+
}
|
|
16301
|
+
return mermaidBlock(lines.join("\n"));
|
|
16302
|
+
}
|
|
16303
|
+
function buildHealthGauge(categories) {
|
|
16304
|
+
const valid = categories.filter((c) => c.total > 0);
|
|
16305
|
+
if (valid.length === 0) {
|
|
16306
|
+
return placeholder("No completeness data available.");
|
|
16307
|
+
}
|
|
16308
|
+
const pies = valid.map((cat) => {
|
|
16309
|
+
const incomplete = cat.total - cat.complete;
|
|
16310
|
+
const lines = [
|
|
16311
|
+
`pie title ${sanitize(cat.name, 30)}`,
|
|
16312
|
+
` "Complete" : ${cat.complete}`,
|
|
16313
|
+
` "Incomplete" : ${incomplete}`
|
|
16314
|
+
];
|
|
16315
|
+
return mermaidBlock(lines.join("\n"));
|
|
16316
|
+
});
|
|
16317
|
+
return `<div class="mermaid-row">${pies.join("\n")}</div>`;
|
|
16318
|
+
}
|
|
16319
|
+
|
|
15693
16320
|
// src/web/templates/pages/overview.ts
|
|
15694
|
-
function
|
|
15695
|
-
|
|
15696
|
-
(t) => `
|
|
16321
|
+
function renderCard(t) {
|
|
16322
|
+
return `
|
|
15697
16323
|
<div class="card">
|
|
15698
16324
|
<a href="/docs/${t.type}">
|
|
15699
16325
|
<div class="card-label">${escapeHtml(typeLabel(t.type))}s</div>
|
|
15700
16326
|
<div class="card-value">${t.total}</div>
|
|
15701
16327
|
${t.open > 0 ? `<div class="card-sub">${t.open} open</div>` : `<div class="card-sub">none open</div>`}
|
|
15702
16328
|
</a>
|
|
15703
|
-
</div
|
|
15704
|
-
|
|
16329
|
+
</div>`;
|
|
16330
|
+
}
|
|
16331
|
+
function overviewPage(data, diagrams, navGroups) {
|
|
16332
|
+
const typeMap = new Map(data.types.map((t) => [t.type, t]));
|
|
16333
|
+
const placed = /* @__PURE__ */ new Set();
|
|
16334
|
+
const groupSections = navGroups.map((group) => {
|
|
16335
|
+
const groupCards = group.types.filter((type) => typeMap.has(type)).map((type) => {
|
|
16336
|
+
placed.add(type);
|
|
16337
|
+
return renderCard(typeMap.get(type));
|
|
16338
|
+
});
|
|
16339
|
+
if (groupCards.length === 0) return "";
|
|
16340
|
+
return `
|
|
16341
|
+
<div class="card-group">
|
|
16342
|
+
<div class="card-group-label">${escapeHtml(group.label)}</div>
|
|
16343
|
+
<div class="cards">${groupCards.join("\n")}</div>
|
|
16344
|
+
</div>`;
|
|
16345
|
+
}).filter(Boolean).join("\n");
|
|
16346
|
+
const ungrouped = data.types.filter((t) => !placed.has(t.type));
|
|
16347
|
+
const ungroupedSection = ungrouped.length > 0 ? `
|
|
16348
|
+
<div class="card-group">
|
|
16349
|
+
<div class="card-group-label">Other</div>
|
|
16350
|
+
<div class="cards">${ungrouped.map(renderCard).join("\n")}</div>
|
|
16351
|
+
</div>` : "";
|
|
15705
16352
|
const rows = data.recent.map(
|
|
15706
16353
|
(doc) => `
|
|
15707
16354
|
<tr>
|
|
@@ -15717,9 +16364,14 @@ function overviewPage(data) {
|
|
|
15717
16364
|
<h2>Project Overview</h2>
|
|
15718
16365
|
</div>
|
|
15719
16366
|
|
|
15720
|
-
|
|
15721
|
-
|
|
15722
|
-
|
|
16367
|
+
${groupSections}
|
|
16368
|
+
${ungroupedSection}
|
|
16369
|
+
|
|
16370
|
+
<div class="section-title">Project Timeline</div>
|
|
16371
|
+
${buildTimelineGantt(diagrams)}
|
|
16372
|
+
|
|
16373
|
+
<div class="section-title">Artifact Relationships</div>
|
|
16374
|
+
${buildArtifactFlowchart(diagrams)}
|
|
15723
16375
|
|
|
15724
16376
|
<div class="section-title">Recent Activity</div>
|
|
15725
16377
|
${data.recent.length > 0 ? `
|
|
@@ -15886,6 +16538,76 @@ function garPage(report) {
|
|
|
15886
16538
|
<div class="gar-areas">
|
|
15887
16539
|
${areaCards}
|
|
15888
16540
|
</div>
|
|
16541
|
+
|
|
16542
|
+
<div class="section-title">Status Distribution</div>
|
|
16543
|
+
${buildStatusPie("Action Status", {
|
|
16544
|
+
Open: report.metrics.scope.open,
|
|
16545
|
+
Done: report.metrics.scope.done,
|
|
16546
|
+
"In Progress": Math.max(0, report.metrics.scope.total - report.metrics.scope.open - report.metrics.scope.done)
|
|
16547
|
+
})}
|
|
16548
|
+
`;
|
|
16549
|
+
}
|
|
16550
|
+
|
|
16551
|
+
// src/web/templates/pages/health.ts
|
|
16552
|
+
function healthPage(report, metrics) {
|
|
16553
|
+
const dotClass = `dot-${report.overall}`;
|
|
16554
|
+
function renderSection(title, categories) {
|
|
16555
|
+
const cards = categories.map(
|
|
16556
|
+
(cat) => `
|
|
16557
|
+
<div class="gar-area">
|
|
16558
|
+
<div class="area-header">
|
|
16559
|
+
<div class="area-dot dot-${cat.status}"></div>
|
|
16560
|
+
<div class="area-name">${escapeHtml(cat.name)}</div>
|
|
16561
|
+
</div>
|
|
16562
|
+
<div class="area-summary">${escapeHtml(cat.summary)}</div>
|
|
16563
|
+
${cat.items.length > 0 ? `<ul>${cat.items.map((item) => `<li><span class="ref-id">${escapeHtml(item.id)}</span>${escapeHtml(item.detail)}</li>`).join("")}</ul>` : ""}
|
|
16564
|
+
</div>`
|
|
16565
|
+
).join("\n");
|
|
16566
|
+
return `
|
|
16567
|
+
<div class="health-section-title">${escapeHtml(title)}</div>
|
|
16568
|
+
<div class="gar-areas">${cards}</div>
|
|
16569
|
+
`;
|
|
16570
|
+
}
|
|
16571
|
+
return `
|
|
16572
|
+
<div class="page-header">
|
|
16573
|
+
<h2>Governance Health Check</h2>
|
|
16574
|
+
<div class="subtitle">Generated ${escapeHtml(report.generatedAt)}</div>
|
|
16575
|
+
</div>
|
|
16576
|
+
|
|
16577
|
+
<div class="gar-overall">
|
|
16578
|
+
<div class="dot ${dotClass}"></div>
|
|
16579
|
+
<div class="label">Overall: ${escapeHtml(report.overall)}</div>
|
|
16580
|
+
</div>
|
|
16581
|
+
|
|
16582
|
+
${renderSection("Completeness", report.completeness)}
|
|
16583
|
+
|
|
16584
|
+
<div class="health-section-title">Completeness Overview</div>
|
|
16585
|
+
${buildHealthGauge(
|
|
16586
|
+
metrics ? Object.entries(metrics.completeness).map(([name, cat]) => ({
|
|
16587
|
+
name: name.replace(/\b\w/g, (c) => c.toUpperCase()),
|
|
16588
|
+
complete: cat.complete,
|
|
16589
|
+
total: cat.total
|
|
16590
|
+
})) : report.completeness.map((c) => {
|
|
16591
|
+
const match = c.summary.match(/(\d+)\s*\/\s*(\d+)/);
|
|
16592
|
+
return {
|
|
16593
|
+
name: c.name,
|
|
16594
|
+
complete: match ? parseInt(match[1], 10) : 0,
|
|
16595
|
+
total: match ? parseInt(match[2], 10) : 0
|
|
16596
|
+
};
|
|
16597
|
+
})
|
|
16598
|
+
)}
|
|
16599
|
+
|
|
16600
|
+
${renderSection("Process", report.process)}
|
|
16601
|
+
|
|
16602
|
+
<div class="health-section-title">Process Summary</div>
|
|
16603
|
+
${metrics ? buildStatusPie("Process Health", {
|
|
16604
|
+
Stale: metrics.process.stale.length,
|
|
16605
|
+
"Aging Actions": metrics.process.agingActions.length,
|
|
16606
|
+
Healthy: Math.max(
|
|
16607
|
+
0,
|
|
16608
|
+
(metrics.completeness ? Object.values(metrics.completeness).reduce((sum, c) => sum + c.total, 0) : 0) - metrics.process.stale.length - metrics.process.agingActions.length
|
|
16609
|
+
)
|
|
16610
|
+
}) : ""}
|
|
15889
16611
|
`;
|
|
15890
16612
|
}
|
|
15891
16613
|
|
|
@@ -15952,7 +16674,8 @@ function handleRequest(req, res, store, projectName, navGroups) {
|
|
|
15952
16674
|
}
|
|
15953
16675
|
if (pathname === "/") {
|
|
15954
16676
|
const data = getOverviewData(store);
|
|
15955
|
-
const
|
|
16677
|
+
const diagrams = getDiagramData(store);
|
|
16678
|
+
const body = overviewPage(data, diagrams, navGroups);
|
|
15956
16679
|
respond(res, layout({ title: "Overview", activePath: "/", projectName, navGroups }, body));
|
|
15957
16680
|
return;
|
|
15958
16681
|
}
|
|
@@ -15962,6 +16685,13 @@ function handleRequest(req, res, store, projectName, navGroups) {
|
|
|
15962
16685
|
respond(res, layout({ title: "GAR Report", activePath: "/gar", projectName, navGroups }, body));
|
|
15963
16686
|
return;
|
|
15964
16687
|
}
|
|
16688
|
+
if (pathname === "/health") {
|
|
16689
|
+
const healthMetrics = collectHealthMetrics(store);
|
|
16690
|
+
const report = evaluateHealth(projectName, healthMetrics);
|
|
16691
|
+
const body = healthPage(report, healthMetrics);
|
|
16692
|
+
respond(res, layout({ title: "Health Check", activePath: "/health", projectName, navGroups }, body));
|
|
16693
|
+
return;
|
|
16694
|
+
}
|
|
15965
16695
|
const boardMatch = pathname.match(/^\/board(?:\/([^/]+))?$/);
|
|
15966
16696
|
if (boardMatch) {
|
|
15967
16697
|
const type = boardMatch[1];
|
|
@@ -16042,7 +16772,7 @@ function createMeetingTools(store) {
|
|
|
16042
16772
|
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
16043
16773
|
};
|
|
16044
16774
|
},
|
|
16045
|
-
{ annotations: {
|
|
16775
|
+
{ annotations: { readOnlyHint: true } }
|
|
16046
16776
|
),
|
|
16047
16777
|
tool7(
|
|
16048
16778
|
"get_meeting",
|
|
@@ -16069,7 +16799,7 @@ function createMeetingTools(store) {
|
|
|
16069
16799
|
]
|
|
16070
16800
|
};
|
|
16071
16801
|
},
|
|
16072
|
-
{ annotations: {
|
|
16802
|
+
{ annotations: { readOnlyHint: true } }
|
|
16073
16803
|
),
|
|
16074
16804
|
tool7(
|
|
16075
16805
|
"create_meeting",
|
|
@@ -16194,7 +16924,7 @@ function createMeetingTools(store) {
|
|
|
16194
16924
|
content: [{ type: "text", text: sections.join("\n") }]
|
|
16195
16925
|
};
|
|
16196
16926
|
},
|
|
16197
|
-
{ annotations: {
|
|
16927
|
+
{ annotations: { readOnlyHint: true } }
|
|
16198
16928
|
)
|
|
16199
16929
|
];
|
|
16200
16930
|
}
|
|
@@ -16219,7 +16949,8 @@ function createReportTools(store) {
|
|
|
16219
16949
|
id: d.frontmatter.id,
|
|
16220
16950
|
title: d.frontmatter.title,
|
|
16221
16951
|
owner: d.frontmatter.owner,
|
|
16222
|
-
priority: d.frontmatter.priority
|
|
16952
|
+
priority: d.frontmatter.priority,
|
|
16953
|
+
dueDate: d.frontmatter.dueDate
|
|
16223
16954
|
})),
|
|
16224
16955
|
completedActions: completedActions.map((d) => ({
|
|
16225
16956
|
id: d.frontmatter.id,
|
|
@@ -16238,7 +16969,7 @@ function createReportTools(store) {
|
|
|
16238
16969
|
content: [{ type: "text", text: JSON.stringify(report, null, 2) }]
|
|
16239
16970
|
};
|
|
16240
16971
|
},
|
|
16241
|
-
{ annotations: {
|
|
16972
|
+
{ annotations: { readOnlyHint: true } }
|
|
16242
16973
|
),
|
|
16243
16974
|
tool8(
|
|
16244
16975
|
"generate_risk_register",
|
|
@@ -16282,7 +17013,7 @@ function createReportTools(store) {
|
|
|
16282
17013
|
content: [{ type: "text", text: JSON.stringify(register, null, 2) }]
|
|
16283
17014
|
};
|
|
16284
17015
|
},
|
|
16285
|
-
{ annotations: {
|
|
17016
|
+
{ annotations: { readOnlyHint: true } }
|
|
16286
17017
|
),
|
|
16287
17018
|
tool8(
|
|
16288
17019
|
"generate_gar_report",
|
|
@@ -16295,7 +17026,7 @@ function createReportTools(store) {
|
|
|
16295
17026
|
content: [{ type: "text", text: JSON.stringify(report, null, 2) }]
|
|
16296
17027
|
};
|
|
16297
17028
|
},
|
|
16298
|
-
{ annotations: {
|
|
17029
|
+
{ annotations: { readOnlyHint: true } }
|
|
16299
17030
|
),
|
|
16300
17031
|
tool8(
|
|
16301
17032
|
"generate_epic_progress",
|
|
@@ -16380,7 +17111,7 @@ function createReportTools(store) {
|
|
|
16380
17111
|
]
|
|
16381
17112
|
};
|
|
16382
17113
|
},
|
|
16383
|
-
{ annotations: {
|
|
17114
|
+
{ annotations: { readOnlyHint: true } }
|
|
16384
17115
|
),
|
|
16385
17116
|
tool8(
|
|
16386
17117
|
"generate_sprint_progress",
|
|
@@ -16425,7 +17156,8 @@ function createReportTools(store) {
|
|
|
16425
17156
|
id: d.frontmatter.id,
|
|
16426
17157
|
title: d.frontmatter.title,
|
|
16427
17158
|
type: d.frontmatter.type,
|
|
16428
|
-
status: d.frontmatter.status
|
|
17159
|
+
status: d.frontmatter.status,
|
|
17160
|
+
dueDate: d.frontmatter.dueDate
|
|
16429
17161
|
}))
|
|
16430
17162
|
}
|
|
16431
17163
|
};
|
|
@@ -16434,7 +17166,7 @@ function createReportTools(store) {
|
|
|
16434
17166
|
content: [{ type: "text", text: JSON.stringify({ sprints }, null, 2) }]
|
|
16435
17167
|
};
|
|
16436
17168
|
},
|
|
16437
|
-
{ annotations: {
|
|
17169
|
+
{ annotations: { readOnlyHint: true } }
|
|
16438
17170
|
),
|
|
16439
17171
|
tool8(
|
|
16440
17172
|
"generate_feature_progress",
|
|
@@ -16474,7 +17206,20 @@ function createReportTools(store) {
|
|
|
16474
17206
|
content: [{ type: "text", text: JSON.stringify({ features }, null, 2) }]
|
|
16475
17207
|
};
|
|
16476
17208
|
},
|
|
16477
|
-
{ annotations: {
|
|
17209
|
+
{ annotations: { readOnlyHint: true } }
|
|
17210
|
+
),
|
|
17211
|
+
tool8(
|
|
17212
|
+
"generate_health_report",
|
|
17213
|
+
"Generate a governance health check report covering artifact completeness and process health metrics",
|
|
17214
|
+
{},
|
|
17215
|
+
async () => {
|
|
17216
|
+
const metrics = collectHealthMetrics(store);
|
|
17217
|
+
const report = evaluateHealth("project", metrics);
|
|
17218
|
+
return {
|
|
17219
|
+
content: [{ type: "text", text: JSON.stringify(report, null, 2) }]
|
|
17220
|
+
};
|
|
17221
|
+
},
|
|
17222
|
+
{ annotations: { readOnlyHint: true } }
|
|
16478
17223
|
),
|
|
16479
17224
|
tool8(
|
|
16480
17225
|
"save_report",
|
|
@@ -16536,7 +17281,7 @@ function createFeatureTools(store) {
|
|
|
16536
17281
|
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
16537
17282
|
};
|
|
16538
17283
|
},
|
|
16539
|
-
{ annotations: {
|
|
17284
|
+
{ annotations: { readOnlyHint: true } }
|
|
16540
17285
|
),
|
|
16541
17286
|
tool9(
|
|
16542
17287
|
"get_feature",
|
|
@@ -16563,7 +17308,7 @@ function createFeatureTools(store) {
|
|
|
16563
17308
|
]
|
|
16564
17309
|
};
|
|
16565
17310
|
},
|
|
16566
|
-
{ annotations: {
|
|
17311
|
+
{ annotations: { readOnlyHint: true } }
|
|
16567
17312
|
),
|
|
16568
17313
|
tool9(
|
|
16569
17314
|
"create_feature",
|
|
@@ -16654,7 +17399,7 @@ function createEpicTools(store) {
|
|
|
16654
17399
|
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
16655
17400
|
};
|
|
16656
17401
|
},
|
|
16657
|
-
{ annotations: {
|
|
17402
|
+
{ annotations: { readOnlyHint: true } }
|
|
16658
17403
|
),
|
|
16659
17404
|
tool10(
|
|
16660
17405
|
"get_epic",
|
|
@@ -16681,7 +17426,7 @@ function createEpicTools(store) {
|
|
|
16681
17426
|
]
|
|
16682
17427
|
};
|
|
16683
17428
|
},
|
|
16684
|
-
{ annotations: {
|
|
17429
|
+
{ annotations: { readOnlyHint: true } }
|
|
16685
17430
|
),
|
|
16686
17431
|
tool10(
|
|
16687
17432
|
"create_epic",
|
|
@@ -16813,7 +17558,7 @@ function createContributionTools(store) {
|
|
|
16813
17558
|
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
16814
17559
|
};
|
|
16815
17560
|
},
|
|
16816
|
-
{ annotations: {
|
|
17561
|
+
{ annotations: { readOnlyHint: true } }
|
|
16817
17562
|
),
|
|
16818
17563
|
tool11(
|
|
16819
17564
|
"get_contribution",
|
|
@@ -16840,7 +17585,7 @@ function createContributionTools(store) {
|
|
|
16840
17585
|
]
|
|
16841
17586
|
};
|
|
16842
17587
|
},
|
|
16843
|
-
{ annotations: {
|
|
17588
|
+
{ annotations: { readOnlyHint: true } }
|
|
16844
17589
|
),
|
|
16845
17590
|
tool11(
|
|
16846
17591
|
"create_contribution",
|
|
@@ -16925,7 +17670,7 @@ function createSprintTools(store) {
|
|
|
16925
17670
|
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
16926
17671
|
};
|
|
16927
17672
|
},
|
|
16928
|
-
{ annotations: {
|
|
17673
|
+
{ annotations: { readOnlyHint: true } }
|
|
16929
17674
|
),
|
|
16930
17675
|
tool12(
|
|
16931
17676
|
"get_sprint",
|
|
@@ -16952,7 +17697,7 @@ function createSprintTools(store) {
|
|
|
16952
17697
|
]
|
|
16953
17698
|
};
|
|
16954
17699
|
},
|
|
16955
|
-
{ annotations: {
|
|
17700
|
+
{ annotations: { readOnlyHint: true } }
|
|
16956
17701
|
),
|
|
16957
17702
|
tool12(
|
|
16958
17703
|
"create_sprint",
|
|
@@ -17249,7 +17994,7 @@ function createSprintPlanningTools(store) {
|
|
|
17249
17994
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
17250
17995
|
};
|
|
17251
17996
|
},
|
|
17252
|
-
{ annotations: {
|
|
17997
|
+
{ annotations: { readOnlyHint: true } }
|
|
17253
17998
|
)
|
|
17254
17999
|
];
|
|
17255
18000
|
}
|
|
@@ -17303,6 +18048,7 @@ var genericAgilePlugin = {
|
|
|
17303
18048
|
- Do NOT create epics \u2014 that is the Tech Lead's responsibility. You can view epics to track progress.
|
|
17304
18049
|
- Use priority levels (critical, high, medium, low) to communicate business value.
|
|
17305
18050
|
- Tag features for categorization and cross-referencing.
|
|
18051
|
+
- Include a \`dueDate\` on actions when target dates are known, to enable schedule tracking and overdue detection.
|
|
17306
18052
|
|
|
17307
18053
|
**Contribution Tools:**
|
|
17308
18054
|
- **list_contributions** / **get_contribution**: Browse and read contribution records.
|
|
@@ -17333,6 +18079,7 @@ var genericAgilePlugin = {
|
|
|
17333
18079
|
- Tag work items (actions, decisions, questions) with \`epic:E-xxx\` to group them under an epic.
|
|
17334
18080
|
- Collaborate with the Delivery Manager on target dates and effort estimates.
|
|
17335
18081
|
- Each epic should have a clear scope and definition of done.
|
|
18082
|
+
- Set \`dueDate\` on technical actions based on sprint timelines or epic target dates. Use the \`sprints\` parameter to assign actions to relevant sprints.
|
|
17336
18083
|
|
|
17337
18084
|
**Contribution Tools:**
|
|
17338
18085
|
- **list_contributions** / **get_contribution**: Browse and read contribution records.
|
|
@@ -17392,6 +18139,11 @@ var genericAgilePlugin = {
|
|
|
17392
18139
|
- **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
18140
|
- Use \`save_report\` with reportType "sprint-progress" to persist sprint reports.
|
|
17394
18141
|
|
|
18142
|
+
**Date Enforcement:**
|
|
18143
|
+
- 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.
|
|
18144
|
+
- When create_action suggests matching sprints in its response, review and assign accordingly using update_action.
|
|
18145
|
+
- Use \`suggest_sprints_for_action\` to find the right sprint for existing actions that lack sprint assignment.
|
|
18146
|
+
|
|
17395
18147
|
**Sprint Workflow:**
|
|
17396
18148
|
- Create sprints with clear goals and date boundaries.
|
|
17397
18149
|
- Assign epics to sprints via linkedEpics.
|
|
@@ -17411,7 +18163,7 @@ var genericAgilePlugin = {
|
|
|
17411
18163
|
**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
18164
|
**Meetings**: Meeting records with attendees, agendas, and notes.
|
|
17413
18165
|
|
|
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.
|
|
18166
|
+
**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
18167
|
|
|
17416
18168
|
- **list_meetings** / **get_meeting**: Browse and read meeting records.
|
|
17417
18169
|
- **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 +18209,7 @@ function createUseCaseTools(store) {
|
|
|
17457
18209
|
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
17458
18210
|
};
|
|
17459
18211
|
},
|
|
17460
|
-
{ annotations: {
|
|
18212
|
+
{ annotations: { readOnlyHint: true } }
|
|
17461
18213
|
),
|
|
17462
18214
|
tool14(
|
|
17463
18215
|
"get_use_case",
|
|
@@ -17484,7 +18236,7 @@ function createUseCaseTools(store) {
|
|
|
17484
18236
|
]
|
|
17485
18237
|
};
|
|
17486
18238
|
},
|
|
17487
|
-
{ annotations: {
|
|
18239
|
+
{ annotations: { readOnlyHint: true } }
|
|
17488
18240
|
),
|
|
17489
18241
|
tool14(
|
|
17490
18242
|
"create_use_case",
|
|
@@ -17582,7 +18334,7 @@ function createTechAssessmentTools(store) {
|
|
|
17582
18334
|
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
17583
18335
|
};
|
|
17584
18336
|
},
|
|
17585
|
-
{ annotations: {
|
|
18337
|
+
{ annotations: { readOnlyHint: true } }
|
|
17586
18338
|
),
|
|
17587
18339
|
tool15(
|
|
17588
18340
|
"get_tech_assessment",
|
|
@@ -17609,7 +18361,7 @@ function createTechAssessmentTools(store) {
|
|
|
17609
18361
|
]
|
|
17610
18362
|
};
|
|
17611
18363
|
},
|
|
17612
|
-
{ annotations: {
|
|
18364
|
+
{ annotations: { readOnlyHint: true } }
|
|
17613
18365
|
),
|
|
17614
18366
|
tool15(
|
|
17615
18367
|
"create_tech_assessment",
|
|
@@ -17743,7 +18495,7 @@ function createExtensionDesignTools(store) {
|
|
|
17743
18495
|
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
17744
18496
|
};
|
|
17745
18497
|
},
|
|
17746
|
-
{ annotations: {
|
|
18498
|
+
{ annotations: { readOnlyHint: true } }
|
|
17747
18499
|
),
|
|
17748
18500
|
tool16(
|
|
17749
18501
|
"get_extension_design",
|
|
@@ -17770,7 +18522,7 @@ function createExtensionDesignTools(store) {
|
|
|
17770
18522
|
]
|
|
17771
18523
|
};
|
|
17772
18524
|
},
|
|
17773
|
-
{ annotations: {
|
|
18525
|
+
{ annotations: { readOnlyHint: true } }
|
|
17774
18526
|
),
|
|
17775
18527
|
tool16(
|
|
17776
18528
|
"create_extension_design",
|
|
@@ -17922,7 +18674,7 @@ function createAemReportTools(store) {
|
|
|
17922
18674
|
]
|
|
17923
18675
|
};
|
|
17924
18676
|
},
|
|
17925
|
-
{ annotations: {
|
|
18677
|
+
{ annotations: { readOnlyHint: true } }
|
|
17926
18678
|
),
|
|
17927
18679
|
tool17(
|
|
17928
18680
|
"generate_tech_readiness",
|
|
@@ -17974,7 +18726,7 @@ function createAemReportTools(store) {
|
|
|
17974
18726
|
]
|
|
17975
18727
|
};
|
|
17976
18728
|
},
|
|
17977
|
-
{ annotations: {
|
|
18729
|
+
{ annotations: { readOnlyHint: true } }
|
|
17978
18730
|
),
|
|
17979
18731
|
tool17(
|
|
17980
18732
|
"generate_phase_status",
|
|
@@ -18029,7 +18781,7 @@ function createAemReportTools(store) {
|
|
|
18029
18781
|
]
|
|
18030
18782
|
};
|
|
18031
18783
|
},
|
|
18032
|
-
{ annotations: {
|
|
18784
|
+
{ annotations: { readOnlyHint: true } }
|
|
18033
18785
|
)
|
|
18034
18786
|
];
|
|
18035
18787
|
}
|
|
@@ -18061,7 +18813,7 @@ function createAemPhaseTools(store, marvinDir) {
|
|
|
18061
18813
|
]
|
|
18062
18814
|
};
|
|
18063
18815
|
},
|
|
18064
|
-
{ annotations: {
|
|
18816
|
+
{ annotations: { readOnlyHint: true } }
|
|
18065
18817
|
),
|
|
18066
18818
|
tool18(
|
|
18067
18819
|
"advance_phase",
|
|
@@ -18506,7 +19258,7 @@ function createJiraTools(store) {
|
|
|
18506
19258
|
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
18507
19259
|
};
|
|
18508
19260
|
},
|
|
18509
|
-
{ annotations: {
|
|
19261
|
+
{ annotations: { readOnlyHint: true } }
|
|
18510
19262
|
),
|
|
18511
19263
|
tool19(
|
|
18512
19264
|
"get_jira_issue",
|
|
@@ -18538,7 +19290,7 @@ function createJiraTools(store) {
|
|
|
18538
19290
|
]
|
|
18539
19291
|
};
|
|
18540
19292
|
},
|
|
18541
|
-
{ annotations: {
|
|
19293
|
+
{ annotations: { readOnlyHint: true } }
|
|
18542
19294
|
),
|
|
18543
19295
|
// --- Jira → Local tools ---
|
|
18544
19296
|
tool19(
|
|
@@ -19269,7 +20021,7 @@ function createWebTools(store, projectName, navGroups) {
|
|
|
19269
20021
|
content: [{ type: "text", text: JSON.stringify(urls, null, 2) }]
|
|
19270
20022
|
};
|
|
19271
20023
|
},
|
|
19272
|
-
{ annotations: {
|
|
20024
|
+
{ annotations: { readOnlyHint: true } }
|
|
19273
20025
|
),
|
|
19274
20026
|
tool20(
|
|
19275
20027
|
"get_dashboard_overview",
|
|
@@ -19291,7 +20043,7 @@ function createWebTools(store, projectName, navGroups) {
|
|
|
19291
20043
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
19292
20044
|
};
|
|
19293
20045
|
},
|
|
19294
|
-
{ annotations: {
|
|
20046
|
+
{ annotations: { readOnlyHint: true } }
|
|
19295
20047
|
),
|
|
19296
20048
|
tool20(
|
|
19297
20049
|
"get_dashboard_gar",
|
|
@@ -19303,7 +20055,7 @@ function createWebTools(store, projectName, navGroups) {
|
|
|
19303
20055
|
content: [{ type: "text", text: JSON.stringify(report, null, 2) }]
|
|
19304
20056
|
};
|
|
19305
20057
|
},
|
|
19306
|
-
{ annotations: {
|
|
20058
|
+
{ annotations: { readOnlyHint: true } }
|
|
19307
20059
|
),
|
|
19308
20060
|
tool20(
|
|
19309
20061
|
"get_dashboard_board",
|
|
@@ -19331,7 +20083,7 @@ function createWebTools(store, projectName, navGroups) {
|
|
|
19331
20083
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
19332
20084
|
};
|
|
19333
20085
|
},
|
|
19334
|
-
{ annotations: {
|
|
20086
|
+
{ annotations: { readOnlyHint: true } }
|
|
19335
20087
|
)
|
|
19336
20088
|
];
|
|
19337
20089
|
}
|
|
@@ -20029,7 +20781,7 @@ ${summaries}`
|
|
|
20029
20781
|
content: [{ type: "text", text: guidance }]
|
|
20030
20782
|
};
|
|
20031
20783
|
},
|
|
20032
|
-
{ annotations: {
|
|
20784
|
+
{ annotations: { readOnlyHint: true } }
|
|
20033
20785
|
)
|
|
20034
20786
|
];
|
|
20035
20787
|
}
|
|
@@ -22633,6 +23385,105 @@ function renderConfluence(report) {
|
|
|
22633
23385
|
return lines.join("\n");
|
|
22634
23386
|
}
|
|
22635
23387
|
|
|
23388
|
+
// src/reports/health/render-ascii.ts
|
|
23389
|
+
import chalk17 from "chalk";
|
|
23390
|
+
var STATUS_DOT2 = {
|
|
23391
|
+
green: chalk17.green("\u25CF"),
|
|
23392
|
+
amber: chalk17.yellow("\u25CF"),
|
|
23393
|
+
red: chalk17.red("\u25CF")
|
|
23394
|
+
};
|
|
23395
|
+
var STATUS_LABEL2 = {
|
|
23396
|
+
green: chalk17.green.bold("GREEN"),
|
|
23397
|
+
amber: chalk17.yellow.bold("AMBER"),
|
|
23398
|
+
red: chalk17.red.bold("RED")
|
|
23399
|
+
};
|
|
23400
|
+
var SEPARATOR2 = chalk17.dim("\u2500".repeat(60));
|
|
23401
|
+
function renderAscii2(report) {
|
|
23402
|
+
const lines = [];
|
|
23403
|
+
lines.push("");
|
|
23404
|
+
lines.push(chalk17.bold(` Health Check \xB7 ${report.projectName}`));
|
|
23405
|
+
lines.push(chalk17.dim(` ${report.generatedAt}`));
|
|
23406
|
+
lines.push("");
|
|
23407
|
+
lines.push(` Overall: ${STATUS_LABEL2[report.overall]}`);
|
|
23408
|
+
lines.push("");
|
|
23409
|
+
lines.push(` ${SEPARATOR2}`);
|
|
23410
|
+
lines.push(chalk17.bold(" Completeness"));
|
|
23411
|
+
lines.push(` ${SEPARATOR2}`);
|
|
23412
|
+
for (const cat of report.completeness) {
|
|
23413
|
+
lines.push(` ${STATUS_DOT2[cat.status]} ${chalk17.bold(cat.name.padEnd(16))} ${cat.summary}`);
|
|
23414
|
+
for (const item of cat.items) {
|
|
23415
|
+
lines.push(` ${chalk17.dim("\u2514")} ${item.id} ${chalk17.dim(item.detail)}`);
|
|
23416
|
+
}
|
|
23417
|
+
}
|
|
23418
|
+
lines.push("");
|
|
23419
|
+
lines.push(` ${SEPARATOR2}`);
|
|
23420
|
+
lines.push(chalk17.bold(" Process"));
|
|
23421
|
+
lines.push(` ${SEPARATOR2}`);
|
|
23422
|
+
for (const cat of report.process) {
|
|
23423
|
+
lines.push(` ${STATUS_DOT2[cat.status]} ${chalk17.bold(cat.name.padEnd(22))} ${cat.summary}`);
|
|
23424
|
+
for (const item of cat.items) {
|
|
23425
|
+
lines.push(` ${chalk17.dim("\u2514")} ${item.id} ${chalk17.dim(item.detail)}`);
|
|
23426
|
+
}
|
|
23427
|
+
}
|
|
23428
|
+
lines.push(` ${SEPARATOR2}`);
|
|
23429
|
+
lines.push("");
|
|
23430
|
+
return lines.join("\n");
|
|
23431
|
+
}
|
|
23432
|
+
|
|
23433
|
+
// src/reports/health/render-confluence.ts
|
|
23434
|
+
var EMOJI2 = {
|
|
23435
|
+
green: ":green_circle:",
|
|
23436
|
+
amber: ":yellow_circle:",
|
|
23437
|
+
red: ":red_circle:"
|
|
23438
|
+
};
|
|
23439
|
+
function renderConfluence2(report) {
|
|
23440
|
+
const lines = [];
|
|
23441
|
+
lines.push(`# Health Check \u2014 ${report.projectName}`);
|
|
23442
|
+
lines.push("");
|
|
23443
|
+
lines.push(`**Date:** ${report.generatedAt}`);
|
|
23444
|
+
lines.push(`**Overall:** ${EMOJI2[report.overall]} ${report.overall.toUpperCase()}`);
|
|
23445
|
+
lines.push("");
|
|
23446
|
+
lines.push("## Completeness");
|
|
23447
|
+
lines.push("");
|
|
23448
|
+
lines.push("| Category | Status | Summary |");
|
|
23449
|
+
lines.push("|----------|--------|---------|");
|
|
23450
|
+
for (const cat of report.completeness) {
|
|
23451
|
+
lines.push(
|
|
23452
|
+
`| ${cat.name} | ${EMOJI2[cat.status]} ${cat.status.toUpperCase()} | ${cat.summary} |`
|
|
23453
|
+
);
|
|
23454
|
+
}
|
|
23455
|
+
lines.push("");
|
|
23456
|
+
for (const cat of report.completeness) {
|
|
23457
|
+
if (cat.items.length === 0) continue;
|
|
23458
|
+
lines.push(`### ${cat.name}`);
|
|
23459
|
+
lines.push("");
|
|
23460
|
+
for (const item of cat.items) {
|
|
23461
|
+
lines.push(`- **${item.id}** ${item.detail}`);
|
|
23462
|
+
}
|
|
23463
|
+
lines.push("");
|
|
23464
|
+
}
|
|
23465
|
+
lines.push("## Process");
|
|
23466
|
+
lines.push("");
|
|
23467
|
+
lines.push("| Metric | Status | Summary |");
|
|
23468
|
+
lines.push("|--------|--------|---------|");
|
|
23469
|
+
for (const cat of report.process) {
|
|
23470
|
+
lines.push(
|
|
23471
|
+
`| ${cat.name} | ${EMOJI2[cat.status]} ${cat.status.toUpperCase()} | ${cat.summary} |`
|
|
23472
|
+
);
|
|
23473
|
+
}
|
|
23474
|
+
lines.push("");
|
|
23475
|
+
for (const cat of report.process) {
|
|
23476
|
+
if (cat.items.length === 0) continue;
|
|
23477
|
+
lines.push(`### ${cat.name}`);
|
|
23478
|
+
lines.push("");
|
|
23479
|
+
for (const item of cat.items) {
|
|
23480
|
+
lines.push(`- **${item.id}** ${item.detail}`);
|
|
23481
|
+
}
|
|
23482
|
+
lines.push("");
|
|
23483
|
+
}
|
|
23484
|
+
return lines.join("\n");
|
|
23485
|
+
}
|
|
23486
|
+
|
|
22636
23487
|
// src/cli/commands/report.ts
|
|
22637
23488
|
async function garReportCommand(options) {
|
|
22638
23489
|
const project = loadProject();
|
|
@@ -22648,6 +23499,20 @@ async function garReportCommand(options) {
|
|
|
22648
23499
|
console.log(renderAscii(report));
|
|
22649
23500
|
}
|
|
22650
23501
|
}
|
|
23502
|
+
async function healthReportCommand(options) {
|
|
23503
|
+
const project = loadProject();
|
|
23504
|
+
const plugin = resolvePlugin(project.config.methodology);
|
|
23505
|
+
const registrations = plugin?.documentTypeRegistrations ?? [];
|
|
23506
|
+
const store = new DocumentStore(project.marvinDir, registrations);
|
|
23507
|
+
const metrics = collectHealthMetrics(store);
|
|
23508
|
+
const report = evaluateHealth(project.config.name, metrics);
|
|
23509
|
+
const format = options.format ?? "ascii";
|
|
23510
|
+
if (format === "confluence") {
|
|
23511
|
+
console.log(renderConfluence2(report));
|
|
23512
|
+
} else {
|
|
23513
|
+
console.log(renderAscii2(report));
|
|
23514
|
+
}
|
|
23515
|
+
}
|
|
22651
23516
|
|
|
22652
23517
|
// src/cli/commands/web.ts
|
|
22653
23518
|
async function webCommand(options) {
|
|
@@ -22664,7 +23529,7 @@ function createProgram() {
|
|
|
22664
23529
|
const program = new Command();
|
|
22665
23530
|
program.name("marvin").description(
|
|
22666
23531
|
"AI-powered product development assistant with Product Owner, Delivery Manager, and Technical Lead personas"
|
|
22667
|
-
).version("0.3.
|
|
23532
|
+
).version("0.3.5");
|
|
22668
23533
|
program.command("init").description("Initialize a new Marvin project in the current directory").action(async () => {
|
|
22669
23534
|
await initCommand();
|
|
22670
23535
|
});
|
|
@@ -22741,6 +23606,12 @@ function createProgram() {
|
|
|
22741
23606
|
).action(async (options) => {
|
|
22742
23607
|
await garReportCommand(options);
|
|
22743
23608
|
});
|
|
23609
|
+
reportCmd.command("health").description("Generate a governance health check report").option(
|
|
23610
|
+
"--format <format>",
|
|
23611
|
+
"Output format: ascii or confluence (default: ascii)"
|
|
23612
|
+
).action(async (options) => {
|
|
23613
|
+
await healthReportCommand(options);
|
|
23614
|
+
});
|
|
22744
23615
|
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
23616
|
await webCommand(options);
|
|
22746
23617
|
});
|